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

114 lines
7.2 KiB
Markdown

# 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
(`<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](operations.md) for installing the plugin per tenant before the PG
gates can pass.