# dev-deploy testing The plugin has five test gates under `test/`. The e2e suite covers the single-server SQLite feature surface (MAIN :3000 paired with TEST :3001); the four Postgres gates cover multi-tenant isolation, mixed-topology push peering, mixed-topology + tenant<->tenant pull peering, and managed-row data sync against the multi-tenant instance on :3002. See also: [operations.md](operations.md) for starting the instances, [peering.md](peering.md), [multitenancy.md](multitenancy.md), [architecture.md](architecture.md). ## Contents - [The five gates](#the-five-gates) - [Postgres gate specifics](#postgres-gate-specifics) - [Running everything](#running-everything) ## The five gates | Gate | File | Backend | What it covers | Prerequisites | Run command | Passes | | --- | --- | --- | --- | --- | --- | --- | | e2e | `test/e2e.js` | SQLite (MAIN :3000 + TEST :3001) | Schema + bootstrap identity, mutation capture, pairing, promote/pull, conflict detect/resolve/merge, table constraints, file propagation, file refs in page layout, page_group, workflow_step, config + menu, plugin-config propagation, managed/starter row data, revert, data_mode locks, machine-endpoint HMAC security, admin auth | Both SQLite servers up; plugin installed + bootstrapped; `admin@local` / `AdminP@ss1` on both | `node test/e2e.js` (from `dev-deploy/`; also `npm test`) | 93 | | mtGate | `test/mtGate.js` | Postgres (:3002) | Phase 0 multi-tenant ISOLATION: each tenant is its own dev-deploy env (distinct env_id), per-tenant Ops/Entities counts, mutating t1 moves only t1's numbers | PG :3002 up; tenants `t1` and `t2` exist; dev-deploy installed per-tenant | `node test/mtGate.js` | 14 | | mixedTopologyGate | `test/mixedTopologyGate.js` | Postgres (:3002) + SQLite MAIN (:3000) | Phase 1 mixed-topology peering: standalone dev (:3000) promotes to tenant t1 on :3002, ops land in t1 not t2; tenant-bound HMAC: a request signed for t1 is rejected (401) at t2, the same request signed for t2 passes auth | MAIN :3000 + PG :3002 up; dev-deploy on MAIN and per-tenant on t1/t2 | `node test/mixedTopologyGate.js` | 12 | | managedRowsGate | `test/managedRowsGate.js` | Postgres (:3002) + `psql` | Phase 2 managed-rows on PG: mark managed adds the hidden `_dd_row_uuid` column and backfills rows; isolation (t2 gets no `_dd_table_modes` row); promote t1 -> t2 recreates the table with the SAME row UUIDs | PG :3002 up; `psql` reachable; tenants `t1`/`t2` with dev-deploy installed per-tenant | `node test/managedRowsGate.js` | 16 | | pullGate | `test/pullGate.js` | Postgres (:3002) + SQLite MAIN (:3000) | Phase 1 PULL peering (reverse of mixedTopologyGate's push): t1 PULLS standalone dev's journal (host-bound HMAC on the GET `/dev-deploy/api/journal` path); isolation (t2 unchanged by t1's pull); idempotent re-pull ("nothing to pull"); tenant<->tenant pull (t2 pulls t1, dev untouched). `apiJournal` serves only first-party ops, so a pull relays a node's OWN ops only -- no transitive loops | MAIN :3000 + PG :3002 up; dev-deploy on MAIN and per-tenant on t1/t2 | `node test/pullGate.js` | 23 | Notes: - The e2e suite runs its tests in order and shares state across them; do not reorder. Assertion failures are caught and counted (one failure does not stop the suite); the runner exits non-zero if any test failed. - Each Postgres gate self-skips with exit 0 (printing `SKIP`) if its required port is not reachable; `managedRowsGate` also skips if `psql` is unavailable. - e2e drives both instances over a mix of SQLite (`sqlite3`), `curl`, and a `saltcorn run-js` shim (`test/sc-exec.js`) that gives the JS body full `require()` access where `saltcorn run-js`'s vm sandbox falls short (for `Field`, `TableConstraint`, `File`, etc.). It logs in as `admin@local` / `AdminP@ss1`, scraping the CSRF token from each form's source GET page. - e2e's "plugin mismatch" section assumes both instances carry identical plugin lists (`base`, `sbadmin2`, `dev-deploy`) so a promote emits no WARNINGS suffix. ## Postgres gate specifics All four PG gates address tenants by Host header (`.localhost.localdomain:3002` selects tenant `tNN`), keep a separate cookie jar per tenant, and bootstrap each tenant admin over HTTP (`admin@.local` / `AdminP@ss1`, creating the first user if needed). ### psql introspection `mtGate`, `mixedTopologyGate`, and `pullGate` observe state entirely over HTTP by scraping the dev-deploy dashboard (`/admin/dev-deploy/`) for the `Env ID`, `Ops recorded`, and `Entities tracked` rows. `managedRowsGate` additionally uses `psql` because the hidden `_dd_row_uuid` column and per-row UUID values are not observable over HTTP. Its psql connection is fixed in the file: | Setting | Value | | --- | --- | | `PGHOST` | `/var/run/postgresql` | | `PGUSER` | `scott` | | `PGDATABASE` | `saltcorn_idp` | | `PGPASSWORD` | `peer` (dummy; socket peer auth ignores it) | It queries `information_schema.columns` for the `_dd_row_uuid` column and reads schema-qualified tenant tables (for example `"t1".""`, `"t1"._dd_entity_ids`, `"t2"._dd_table_modes`) to confirm the column add/backfill, per-tenant isolation, and that promoted rows carry the SAME UUIDs across tenants. It cleans up its probe table and `_dd_*` rows in both tenant schemas on exit. ### The themeless-tenant sendWrap 500 gotcha A freshly created tenant has no theme/layout configured, so Saltcorn's own admin pages that call `res.sendWrap` (for example `GET /table/new/`) return HTTP 500. dev-deploy's admin pages self-render, so the PG gates grab the per-session `_csrf` token from a dev-deploy page (`GET /admin/dev-deploy/peers`) rather than from `/table/new`. The actual mutating POST (`POST /table`, `POST /table/delete/`) only mutates and redirects (no `sendWrap`), so it succeeds on a themeless tenant and journals the expected op (for example `create_table`). This same trick is used to obtain CSRF for dev-deploy's own admin POSTs (`/admin/dev-deploy/peers/add`, `/promote`, `/tables/set`, etc.). The gates also use idempotent cleanup so reruns start clean: peer rows are cleared by env_id before pairing, and `managedRowsGate` uses a timestamped table name (`mr`) and drops it afterward. `pullGate` likewise uses timestamped probe tables (`pull_dev_`, `pull_t1_`) but drops each ONLY on its SOURCE instance: a drop is a first-party op too, so dropping on the source keeps the journal create->drop linear and the next run's pull replays it in order to clean up the pulled copy. Dropping a PULLED copy instead would author a competing local op and the next run would legitimately conflict with it. ## Running everything ``` cd ~/claude/saltcorn ./startServer.sh & # MAIN :3000 ./startServerTest.sh & # TEST :3001 ./startServerPg.sh & # PG multi-tenant :3002 cd dev-deploy node test/e2e.js # 93 node test/mtGate.js # 14 (PG) node test/mixedTopologyGate.js # 12 (MAIN + PG) node test/managedRowsGate.js # 16 (PG + psql) node test/pullGate.js # 23 (MAIN + PG) ``` The PG gates self-skip if :3002 (or, for `managedRowsGate`, `psql`) is not available, so on a SQLite-only setup only the e2e suite runs its assertions. See [operations.md](operations.md) for installing the plugin per tenant before the PG gates can pass.