138 lines
7.7 KiB
Markdown
138 lines
7.7 KiB
Markdown
# 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: <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](./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.
|