/** * TASK-0131 — SSO provisioning matrix (unit, mocked Prisma/Auth/Audit). * * Covers the match-or-create branches (existing / linked / created / * jit_disabled), the SUSPENDED hard-stop, the org-membership + * workspace auto-join (never demote, only the SSO org's workspaces), the * `authMethod:"sso"` session mint, and the `sso.login.*` / * `sso.login_failed` audit lines. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { ApiError } from "../../../common/errors.js"; import { SsoProvisioningService, type SsoProvisioningInput } from "../provisioning.service.js"; interface PrismaMock { organization: { findUnique: ReturnType }; user: { findUnique: ReturnType; create: ReturnType; update: ReturnType }; ssoIdentity: { findUnique: ReturnType; create: ReturnType }; organizationMember: { findUnique: ReturnType; create: ReturnType }; workspace: { findMany: ReturnType }; workspaceMember: { findMany: ReturnType; createMany: ReturnType }; } function mockPrisma(): PrismaMock { return { organization: { findUnique: vi.fn() }, user: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn() }, ssoIdentity: { findUnique: vi.fn(), create: vi.fn() }, organizationMember: { findUnique: vi.fn(), create: vi.fn() }, workspace: { findMany: vi.fn() }, workspaceMember: { findMany: vi.fn(), createMany: vi.fn() } }; } const ORG_ID = "org_1"; const PROVIDER = { id: "sso_1", type: "SAML" as const, name: "Acme Okta", config: {} }; const TOKENS = { accessToken: "at", refreshToken: "rt", expiresIn: 900, session: { id: "sess_1", expiresAt: new Date("2026-12-31T00:00:00Z") } }; function makeService(prisma: PrismaMock) { const audit = { audit: vi.fn().mockResolvedValue(undefined) }; const auth = { issueTokens: vi.fn().mockResolvedValue(TOKENS) }; const secrets = { getSecrets: vi.fn(), saveSecrets: vi.fn() }; const svc = new SsoProvisioningService(prisma as never, audit as never, auth as never, secrets as never); return { svc, audit, auth }; } function input(over: Partial = {}): SsoProvisioningInput { return { req: { ip: "1.2.3.4", headers: { "user-agent": "test" } } as never, res: {} as never, organizationId: ORG_ID, provider: PROVIDER, externalId: "ext-42", email: "alice@acme.test", ...over }; } beforeEach(() => vi.clearAllMocks()); describe("TASK-0131 SSO provisioning matrix", () => { it("links to an EXISTING SsoIdentity (provisioned=existing) and mints an SSO session", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: {} }); prisma.ssoIdentity.findUnique.mockResolvedValue({ userId: "u_1" }); prisma.user.findUnique.mockResolvedValue({ id: "u_1", email: "alice@acme.test", name: "Alice", displayName: "Alice", status: "ACTIVE" }); prisma.organizationMember.findUnique.mockResolvedValue({ role: "MEMBER" }); prisma.workspace.findMany.mockResolvedValue([{ id: "w_1" }]); prisma.workspaceMember.findMany.mockResolvedValue([{ workspaceId: "w_1" }]); prisma.user.update.mockResolvedValue({}); const { svc, auth, audit } = makeService(prisma); const res = await svc.provision(input()); expect(res.provisioned).toBe("existing"); expect(auth.issueTokens).toHaveBeenCalledWith(expect.objectContaining({ id: "u_1" }), { req: expect.anything(), res: expect.anything(), ip: "1.2.3.4", userAgent: "test", authMethod: "sso" }); // success audit with the provisioned branch const actions = audit.audit.mock.calls.map((c) => c[2].action); expect(actions).toContain("sso.login.saml"); const login = audit.audit.mock.calls.find((c) => c[2].action === "sso.login.saml")!; expect(login[2].metadata.provisioned).toBe("existing"); // membership exists → NOT recreated expect(prisma.organizationMember.create).not.toHaveBeenCalled(); }); it("LINKS a known email that has no SsoIdentity (provisioned=linked)", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: {} }); prisma.ssoIdentity.findUnique.mockResolvedValue(null); prisma.user.findUnique.mockResolvedValue({ id: "u_2", email: "alice@acme.test", name: "Alice", displayName: null, status: "ACTIVE" }); prisma.ssoIdentity.create.mockResolvedValue({}); prisma.organizationMember.findUnique.mockResolvedValue(null); prisma.organizationMember.create.mockResolvedValue({}); prisma.workspace.findMany.mockResolvedValue([]); prisma.workspaceMember.findMany.mockResolvedValue([]); prisma.user.update.mockResolvedValue({}); const { svc } = makeService(prisma); const res = await svc.provision(input()); expect(res.provisioned).toBe("linked"); expect(prisma.ssoIdentity.create).toHaveBeenCalledWith({ data: { providerId: "sso_1", externalId: "ext-42", userId: "u_2" } }); }); it("JIT-CREATES a passwordless verified user (provisioned=created)", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: {} }); prisma.ssoIdentity.findUnique.mockResolvedValue(null); prisma.user.findUnique.mockResolvedValue(null); prisma.user.create.mockResolvedValue({ id: "u_3", email: "alice@acme.test", name: "alice", displayName: "Alice", status: "ACTIVE" }); prisma.ssoIdentity.create.mockResolvedValue({}); prisma.organizationMember.findUnique.mockResolvedValue(null); prisma.organizationMember.create.mockResolvedValue({}); prisma.workspace.findMany.mockResolvedValue([{ id: "w_1" }, { id: "w_2" }]); prisma.workspaceMember.findMany.mockResolvedValue([{ workspaceId: "w_1" }]); prisma.workspaceMember.createMany.mockResolvedValue({ count: 1 }); prisma.user.update.mockResolvedValue({}); const { svc } = makeService(prisma); const res = await svc.provision(input({ displayName: "Alice" })); expect(res.provisioned).toBe("created"); // passwordless + verified expect(prisma.user.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ email: "alice@acme.test", passwordHash: null, emailVerification: "VERIFIED" }) }) ); // auto-joins ONLY the missing org workspace (w_2) expect(prisma.workspaceMember.createMany).toHaveBeenCalledWith({ data: [expect.objectContaining({ workspaceId: "w_2", role: "MEMBER" })], skipDuplicates: true }); // org membership at the policy default role expect(prisma.organizationMember.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ role: "MEMBER" }) }) ); }); it("honors ssoDefaultRole=GUEST (never demotes: no create when a membership exists)", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: { ssoDefaultRole: "GUEST" } }); prisma.ssoIdentity.findUnique.mockResolvedValue({ userId: "u_1" }); prisma.user.findUnique.mockResolvedValue({ id: "u_1", email: "a@b.test", name: "A", displayName: null, status: "ACTIVE" }); prisma.organizationMember.findUnique.mockResolvedValue(null); prisma.organizationMember.create.mockResolvedValue({}); prisma.workspace.findMany.mockResolvedValue([]); prisma.workspaceMember.findMany.mockResolvedValue([]); prisma.user.update.mockResolvedValue({}); const { svc } = makeService(prisma); await svc.provision(input()); expect(prisma.organizationMember.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ role: "GUEST" }) }) ); }); it("rejects a SUSPENDED user (audit sso.login_failed reason=suspended, 403)", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: {} }); prisma.ssoIdentity.findUnique.mockResolvedValue({ userId: "u_1" }); prisma.user.findUnique.mockResolvedValue({ id: "u_1", email: "a@b.test", name: "A", displayName: null, status: "SUSPENDED" }); const { svc, audit } = makeService(prisma); await expect(svc.provision(input())).rejects.toMatchObject({ code: "FORBIDDEN", httpStatus: 403 }); const failed = audit.audit.mock.calls.find((c) => c[2].action === "sso.login_failed"); expect(failed?.[2].metadata.reason).toBe("suspended"); }); it("rejects a foreign email when JIT is disabled (reason=jit_disabled)", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: { ssoJitProvisioning: false } }); prisma.ssoIdentity.findUnique.mockResolvedValue(null); prisma.user.findUnique.mockResolvedValue(null); const { svc, audit } = makeService(prisma); await expect(svc.provision(input())).rejects.toBeInstanceOf(ApiError); const failed = audit.audit.mock.calls.find((c) => c[2].action === "sso.login_failed"); expect(failed?.[2].metadata.reason).toBe("jit_disabled"); }); it("audits sso.login.oidc for OIDC providers (protocol in the action)", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: {} }); prisma.ssoIdentity.findUnique.mockResolvedValue({ userId: "u_1" }); prisma.user.findUnique.mockResolvedValue({ id: "u_1", email: "a@b.test", name: "A", displayName: null, status: "ACTIVE" }); prisma.organizationMember.findUnique.mockResolvedValue({}); prisma.workspace.findMany.mockResolvedValue([]); prisma.workspaceMember.findMany.mockResolvedValue([]); prisma.user.update.mockResolvedValue({}); const { svc, audit } = makeService(prisma); await svc.provision(input({ provider: { ...PROVIDER, type: "OIDC" } })); expect(audit.audit.mock.calls.some((c) => c[2].action === "sso.login.oidc")).toBe(true); }); it("lowercases the email for the account lookup (linked branch)", async () => { const prisma = mockPrisma(); prisma.organization.findUnique.mockResolvedValue({ policy: {} }); prisma.ssoIdentity.findUnique.mockResolvedValue(null); prisma.user.findUnique.mockResolvedValue({ id: "u_1", email: "alice@acme.test", name: "A", displayName: null, status: "ACTIVE" }); prisma.ssoIdentity.create.mockResolvedValue({}); prisma.organizationMember.findUnique.mockResolvedValue({}); prisma.workspace.findMany.mockResolvedValue([]); prisma.workspaceMember.findMany.mockResolvedValue([]); prisma.user.update.mockResolvedValue({}); const { svc } = makeService(prisma); await svc.provision(input({ email: "ALICE@acme.test" })); // the account lookup used the LOWERCASED email expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { email: "alice@acme.test" } }); }); });