import { describe, it, expect } from "vitest"; import { ZodError } from "zod"; import { RecordingEventBus, RecordingConsent, } from "@repo/core-testing/instrumentation"; import { signUpUseCase, signUpOutputSchema, } from "@/application/use-cases/sign-up.use-case"; import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock"; import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock"; import { AuthenticationError } from "@/entities/errors/auth"; import type { IAuthenticationService } from "@/application/services/authentication.service.interface"; import { userFactory } from "@/__factories__/user.factory"; describe("signUpUseCase", () => { it("creates a new user and returns session + cookie", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); const useCase = signUpUseCase(users, auth, bus, undefined); const result = await useCase({ username: "carol", password: "secret_password", confirmPassword: "secret_password", }); expect(result.session.userId).toBeTruthy(); expect(result.cookie.name).toBe("session"); }); it("throws AuthenticationError when username taken", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); await users.createUser(userFactory.build({ username: "alice" })); const useCase = signUpUseCase(users, auth, bus, undefined); await expect( useCase({ username: "alice", password: "secret_password", confirmPassword: "secret_password", }), ).rejects.toBeInstanceOf(AuthenticationError); }); it("publishes auth.user.signed-up after creating the user", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); const useCase = signUpUseCase(users, auth, bus, undefined); await useCase({ username: "dave", password: "secret_password", confirmPassword: "secret_password", }); expect(bus.published).toHaveLength(1); const published = bus.published[0]!; expect(published.name).toBe("auth.user.signed-up"); expect(published.payload).toEqual( expect.objectContaining({ userId: expect.any(String), email: expect.stringMatching(/^dave@/), }), ); }); it("works without an event bus (welcome email skipped silently)", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const useCase = signUpUseCase(users, auth, undefined, undefined); const result = await useCase({ username: "frank", password: "secret_password", confirmPassword: "secret_password", }); expect(result.session.userId).toBeTruthy(); expect(result.cookie.name).toBe("session"); }); it("does NOT publish when sign-up fails (username taken)", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); await users.createUser(userFactory.build({ username: "eve" })); const useCase = signUpUseCase(users, auth, bus, undefined); await expect( useCase({ username: "eve", password: "secret_password", confirmPassword: "secret_password", }), ).rejects.toBeInstanceOf(AuthenticationError); expect(bus.published).toHaveLength(0); }); it("migrates anonymous consent when cc_consent cookie is present", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); const consent = new RecordingConsent(); const consentFactory = (_userId: string) => Promise.resolve(consent); const useCase = signUpUseCase(users, auth, bus, consentFactory); const result = await useCase({ username: "grace", password: "secret_password", confirmPassword: "secret_password", cookieHeader: "cc_consent=necessary,analytics; session=xyz", }); expect(consent.grants).toHaveLength(2); expect(consent.grants[0]).toEqual({ category: "necessary", meta: { method: "signup-migration" }, }); expect(consent.grants[1]).toEqual({ category: "analytics", meta: { method: "signup-migration" }, }); expect(result.clearCookie).toBeDefined(); expect(result.clearCookie?.name).toBe("cc_consent"); expect(result.clearCookie?.attributes.maxAge).toBe(0); }); it("does not migrate consent when no cc_consent cookie is present", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); const consent = new RecordingConsent(); const consentFactory = (_userId: string) => Promise.resolve(consent); const useCase = signUpUseCase(users, auth, bus, consentFactory); const result = await useCase({ username: "henry", password: "secret_password", confirmPassword: "secret_password", cookieHeader: "session=xyz", }); expect(consent.grants).toHaveLength(0); expect(result.clearCookie).toBeUndefined(); }); it("does not migrate consent when consentFactory is absent", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); const useCase = signUpUseCase(users, auth, bus, undefined); const result = await useCase({ username: "iris", password: "secret_password", confirmPassword: "secret_password", cookieHeader: "cc_consent=analytics", }); expect(result.clearCookie).toBeUndefined(); }); it("does not migrate consent when cc_consent cookie has no value", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); const consent = new RecordingConsent(); const consentFactory = (_userId: string) => Promise.resolve(consent); const useCase = signUpUseCase(users, auth, bus, consentFactory); const result = await useCase({ username: "jake", password: "secret_password", confirmPassword: "secret_password", cookieHeader: "cc_consent=", }); expect(consent.grants).toHaveLength(0); expect(result.clearCookie).toBeUndefined(); }); it("parses cookie header with malformed parts (no = sign)", async () => { const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const bus = new RecordingEventBus(); const consent = new RecordingConsent(); const consentFactory = (_userId: string) => Promise.resolve(consent); const useCase = signUpUseCase(users, auth, bus, consentFactory); const result = await useCase({ username: "kate", password: "secret_password", confirmPassword: "secret_password", cookieHeader: "malformedcookie; cc_consent=necessary", }); expect(consent.grants).toHaveLength(1); expect(result.clearCookie).toBeDefined(); }); }); describe("signUpUseCase output validation", () => { it("throws when authenticationService returns a malformed session", async () => { const users = new MockUsersRepository([]); const auth = { hashPassword: async () => "hashed_x", generateUserId: () => "uid1", verifyPassword: async () => true, // session missing required fields → should fail signUpOutputSchema.parse createSession: async () => ({ session: { id: 123 }, cookie: null }), } as unknown as IAuthenticationService; const bus = new RecordingEventBus(); const useCase = signUpUseCase(users, auth, bus, undefined); await expect( useCase({ username: "carol", password: "secret_password", confirmPassword: "secret_password", }), ).rejects.toBeInstanceOf(ZodError); }); it("exports an output schema that mirrors the success shape", () => { expect(signUpOutputSchema).toBeDefined(); const parsed = signUpOutputSchema.safeParse({ session: { id: "s1", userId: "u1", expiresAt: new Date() }, cookie: { name: "session", value: "s1", attributes: {} }, }); expect(parsed.success).toBe(true); }); });