# Testing The `saltcorn-idp` test suite is a set of five end-to-end "gates", one per protocol surface. Each gate is a standalone Node script under `test/` that drives a live, already-running Saltcorn instance over the wire (HTTP, LDAPS, or SAML) and asserts on real responses. There is no mocking: a gate is a black box client, so a passing gate means the deployed plugin actually behaves. The gates assume the local dev instances described in [operations.md](./operations.md) (MAIN on `:3000`, TEST on `:3001`, PG on `:3002`). See the [README](../README.md) for the plugin overview. ## Conventions shared by all gates - Each gate prints `PASS`/`FAIL` per check and ends with a `RESULT: passed, failed` line. - Exit code: `0` when all checks pass (or a gate self-skips), `1` when any check fails, `2` on an uncaught error (a thrown exception, e.g. a server that is down and not handled by a self-skip). - The admin credentials used throughout are `admin@local` / `AdminP@ss1` (single-tenant gates) or `admin@.local` / `AdminP@ss1` (multi-tenant gates). The relying-party/SP redirect and callback targets are fixed test values (`http://localhost:9099/...`) and do not need a real listener there -- the gates only inspect the redirect/form, they never follow the callback. - Several gates register their own client/SP/group via the admin UI (`/admin/idp/...`). Each does a delete-then-create at the start, so re-running a gate is safe even if a prior run left an artifact behind. `e2e.js` and `mtGate.js` also delete their clients again at the end; `samlGate.js` leaves its registered SP in place (the next run's delete-then-create absorbs it). ## The five gates | Gate file | Covers | Prerequisites (instance / port) | Backend | Self-skip when port unreachable | Run command | |-----------|--------|---------------------------------|---------|---------------------------------|-------------| | `test/e2e.js` | OIDC: discovery + JWKS on both instances; client registration via admin UI; authorization-code + PKCE + consent; token + `id_token` RS256 verify; userinfo; groups claim (`role:admin` + custom `group:` claim); confidential-client one-time secret | MAIN `:3000` and TEST `:3001` both up | SQLite | No -- throws (exit 2) if an instance is down | `node test/e2e.js` (or `npm test`) | | `test/ldapGate.js` | LDAP (Phase 4): LDAPS simple bind vs bcrypt hash; authenticated user search (`mail`, `memberOf`); wrong-password + anonymous-search rejection; case-insensitive attribute selection; `groupOfNames` entries; filter-depth rejection; per-message inbound byte cap (DoS); configurable service account (search-then-bind); RFC 4514 DN escaping + `uidFromDn` unescape (unit) | MAIN `:3000` up AND its LDAPS listener on `:1636` | SQLite | No -- if LDAPS is down the bind checks fail (exit 1) rather than self-skip | `node test/ldapGate.js` | | `test/samlGate.js` | SAML (Phase 5): IdP metadata; SP-initiated SSO (signed assertion + real `AuthnStatement`); SP registry + ACS allow-list (unregistered SP and out-of-allow-list ACS rejected, 403); DTD/ENTITY (XXE) rejection (400); decompression-bomb rejection (400); empty-ACS SP refused at registration; signed-SP AuthnRequest accepted + unsigned rejected (403) for a cert-registered SP; IdP-initiated SSO (no `InResponseTo`); Single Logout incl. no-session and wrong-NameID rejection (403) (LAST -- destroys the session) | MAIN `:3000` up | SQLite | No -- throws (exit 2) if MAIN is down | `node test/samlGate.js` | | `test/mtGate.js` | Multi-tenant OIDC: distinct per-tenant issuers + signing keys; full auth-code + PKCE flow on `t1`; cross-tenant isolation -- a `t1` code, access token, and `id_token` are all REJECTED at `t2` (per-tenant store / Provider / key); positive control that the `t1` token still works at `t1` | PG `:3002` up, tenants `t1` + `t2` present with the plugin installed | Postgres | No explicit skip -- throws (exit 2) if PG/tenants are unavailable | `node test/mtGate.js` (or `npm run test:mt`) | | `test/ldapMultiTenantGate.js` | Multi-tenant LDAP: tenant-in-DN binding (`dc=,dc=saltcorn,dc=local`); correct-tenant bind + search; same uid under another tenant fails; cross-tenant search denied; unknown tenant denied; tenant-scoped `groupOfNames` member DNs. Bootstraps each tenant admin over HTTP first | PG `:3002` up AND its LDAPS listener on `:1637` | Postgres | Yes -- if `127.0.0.1:1637` is not reachable it prints `SKIP` and exits 0 (no-op on SQLite-only setups) | `node test/ldapMultiTenantGate.js` | ### Expected pass counts When run against a correctly configured instance, each gate currently reports: | Gate | Expected passes | |------|-----------------| | `test/e2e.js` | 28 | | `test/ldapGate.js` | 22 | | `test/samlGate.js` | 28 | | `test/mtGate.js` | 15 | | `test/ldapMultiTenantGate.js` | 8 | A gate that self-skips (only `ldapMultiTenantGate.js` does this) reports `0 passed, 0 failed (skipped)` and exits 0; that is not a failure, but it also does not contribute its 8 passes. ### Ordering and shared state Checks inside a gate run sequentially and share login/session state -- they are not independent unit tests: - `e2e.js` is organized as Phase 0..3 and reuses one admin session and cookie jar across phases. The Phase 1 client registration is a prerequisite for the Phase 1/2 auth-code flows. - `samlGate.js` logs in once, registers the test SP, then runs SSO checks; the Single Logout check is intentionally LAST because it destroys the session. - `mtGate.js` keeps a separate cookie jar per tenant and depends on the positive `t1` flow having produced a token before the cross-tenant rejection checks run. - `ldapMultiTenantGate.js` bootstraps both tenant admins over HTTP before any LDAP bind. Do not reorder checks within a gate. ## npm scripts From `package.json`: | Script | Command | Gate | |--------|---------|------| | `npm test` | `node test/e2e.js` | OIDC e2e gate (MAIN + TEST, SQLite) | | `npm run test:mt` | `node test/mtGate.js` | multi-tenant OIDC gate (PG) | The three remaining gates (`ldapGate.js`, `samlGate.js`, `ldapMultiTenantGate.js`) have no npm script; run them directly with `node` as shown in the table above. ## Running the full suite The PG gates self-skip or fail fast when their backend is unreachable, so which servers you start determines which gates can pass. See [operations.md](./operations.md) for how to start each instance and install the plugin. 1. **SQLite gates (`e2e`, `ldapGate`, `samlGate`).** Start MAIN and TEST: ```bash ./startServer.sh # MAIN -> :3000 (LDAPS :1636) ./startServerTest.sh # TEST -> :3001 (no LDAP) ``` - `e2e.js` needs both MAIN and TEST (Phase 0 checks discovery/JWKS on each). - `ldapGate.js` needs MAIN plus its LDAPS listener on `:1636` (MAIN's `env.sh` sets `SALTCORN_IDP_LDAP_PORT=1636`). - `samlGate.js` needs MAIN only. Then: ```bash npm test # e2e node test/ldapGate.js node test/samlGate.js ``` 2. **Postgres gates (`mtGate`, `ldapMultiTenantGate`).** Start PG and make sure tenants `t1` and `t2` exist with the plugin installed per tenant (see operations.md, "PG (multi-tenant): per-tenant install"): ```bash ./startServerPg.sh # PG -> :3002 (LDAPS :1637) ``` Then: ```bash npm run test:mt # mtGate (HTTP :3002) node test/ldapMultiTenantGate.js # PG LDAPS :1637 ``` `ldapMultiTenantGate.js` probes `127.0.0.1:1637` first and self-skips (exit 0) if the LDAPS listener is not up -- which is also the workaround surface for the intermittent `:1637` bind flake documented in operations.md. `mtGate.js` has no self-skip and will exit 2 if PG or the tenants are not available, so only run it once PG is confirmed up. If you do not need the multi-tenant coverage, running only the SQLite gates is a valid quick pass.