sc-dev-deploy/docs/testing.md
2026-06-01 16:43:43 -05:00

7.2 KiB

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 for starting the instances, peering.md, multitenancy.md, architecture.md.

Contents

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 (<tNN>.localhost.localdomain:3002 selects tenant tNN), keep a separate cookie jar per tenant, and bootstrap each tenant admin over HTTP (admin@<tNN>.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"."<tbl>", "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/<id>) 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<Date.now()>) and drops it afterward. pullGate likewise uses timestamped probe tables (pull_dev_<ts>, pull_t1_<ts>) 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 for installing the plugin per tenant before the PG gates can pass.