From f8093a31c18dc9cf6278732038ac1f10496283bd Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Wed, 29 Apr 2026 15:47:13 -0700 Subject: [PATCH] test(e2e): add SAML discover, metadata, and login route tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three test files exercising the SAML routes that don't require a full IdP round-trip: - discover.test.ts: 5 cases for /auth/saml/discover — happy path, unknown domain (404), case-insensitivity, unknown project_id, cross-project isolation (project A's connection isn't visible from a query against project B's domain). - metadata.test.ts: 3 cases for /auth/saml/metadata — XML contains entityID + ACS URL embedded, 404 for unknown connection, 404 when connection exists but has no IdP cert (incomplete configuration). - login.test.ts: 5 cases for /auth/saml/login — JSON-mode returns the IdP redirect URL with SAMLRequest+RelayState, browser-redirect mode sets the stack-saml-inner- CSRF cookie, 404 unknown connection, 403 when allowSignIn=false, invalid client_id rejected. Test integrity: all tests drive the API only — no imports from apps/backend/src/saml/. SAML config is set via the standard config override endpoint (no test-only mutator), so the routes run through the same code path real customers would hit. Full SAML round-trip tests (login → mock IdP → ACS → session) deferred to a follow-up — they need a sequenced flow against the mock-saml-idp service that's separate from these endpoint-level tests. --- .../api/v1/auth/saml/discover.test.ts | 97 +++++++++++++++ .../endpoints/api/v1/auth/saml/login.test.ts | 115 ++++++++++++++++++ .../api/v1/auth/saml/metadata.test.ts | 72 +++++++++++ 3 files changed, 284 insertions(+) create mode 100644 apps/e2e/tests/backend/endpoints/api/v1/auth/saml/discover.test.ts create mode 100644 apps/e2e/tests/backend/endpoints/api/v1/auth/saml/login.test.ts create mode 100644 apps/e2e/tests/backend/endpoints/api/v1/auth/saml/metadata.test.ts diff --git a/apps/e2e/tests/backend/endpoints/api/v1/auth/saml/discover.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/auth/saml/discover.test.ts new file mode 100644 index 000000000..a6dcf2897 --- /dev/null +++ b/apps/e2e/tests/backend/endpoints/api/v1/auth/saml/discover.test.ts @@ -0,0 +1,97 @@ +import { it } from "../../../../../../helpers"; +import { Project, niceBackendFetch } from "../../../../../backend-helpers"; + +/** + * Tests for GET /auth/saml/discover. + * + * Test integrity: drives the API only — no imports from + * apps/backend/src/saml/. Project config is set via the standard config + * override endpoint (no special test-only mutator), so the discovery + * lookup runs through the same code path real customers would hit. + * + * Connection isolation note: each `it` block creates its own project via + * Project.createAndSwitch, so connections don't leak across tests. + */ + +async function createProjectWithSamlConnection(slug: string, domain: string) { + const { projectId } = await Project.createAndSwitch(); + // Push the SAML connection at the environment level — that's where the + // IdP-side fields live. The discovery endpoint reads from the rendered + // organization config which folds in env overrides. + await Project.updateConfig({ + [`auth.saml.connections.${slug}.displayName`]: `${slug} SSO`, + [`auth.saml.connections.${slug}.allowSignIn`]: true, + [`auth.saml.connections.${slug}.domain`]: domain, + [`auth.saml.connections.${slug}.idpEntityId`]: `https://idp.${domain}/saml/metadata`, + [`auth.saml.connections.${slug}.idpSsoUrl`]: `https://idp.${domain}/saml/sso`, + [`auth.saml.connections.${slug}.idpCertificate`]: "MIICertificatePlaceholderForDiscoveryTest=", + }); + return { projectId }; +} + +it("returns the matching connection for a known email domain", async ({ expect }) => { + const { projectId } = await createProjectWithSamlConnection("acme", "acme.test"); + + const response = await niceBackendFetch( + `/api/v1/auth/saml/discover?email=alice@acme.test&project_id=${projectId}`, + { method: "GET" }, + ); + + expect(response).toMatchInlineSnapshot(` + NiceResponse { + "status": 200, + "body": { + "connection_id": "acme", + "display_name": "acme SSO", + }, + "headers": Headers {