sc-idp/docs/testing.md
2026-06-01 16:40:54 -05:00

7.7 KiB

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 (MAIN on :3000, TEST on :3001, PG on :3002). See the README for the plugin overview.

Conventions shared by all gates

  • Each gate prints PASS/FAIL per check and ends with a RESULT: <n> passed, <m> 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@<tenant>.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=<tenant>,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 for how to start each instance and install the plugin.

  1. SQLite gates (e2e, ldapGate, samlGate). Start MAIN and TEST:

    ./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:

    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"):

    ./startServerPg.sh      # PG -> :3002  (LDAPS :1637)
    

    Then:

    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.