diff --git a/packages/auth/src/interface-adapters/controllers/sign-in.controller.test.ts b/packages/auth/src/interface-adapters/controllers/sign-in.controller.test.ts new file mode 100644 index 0000000..1959d02 --- /dev/null +++ b/packages/auth/src/interface-adapters/controllers/sign-in.controller.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { authContainer } from "@/di/container"; +import { AUTH_SYMBOLS } from "@/di/symbols"; +import { MockUsersRepository } from "@/infrastructure/repositories/mock-users.repository"; +import { MockAuthenticationService } from "@/infrastructure/services/mock-authentication.service"; +import type { IUsersRepository } from "@/application/repositories/users-repository.interface"; +import type { IAuthenticationService } from "@/application/services/authentication-service.interface"; +import { InputParseError } from "@/entities/errors"; +import { signInController } from "./sign-in.controller"; + +describe("signInController", () => { + let usersRepo: MockUsersRepository; + let authService: MockAuthenticationService; + + beforeEach(() => { + if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) { + authContainer.unbind(AUTH_SYMBOLS.IUsersRepository); + } + if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) { + authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService); + } + usersRepo = new MockUsersRepository(); + authService = new MockAuthenticationService(usersRepo); + authContainer + .bind(AUTH_SYMBOLS.IUsersRepository) + .toConstantValue(usersRepo); + authContainer + .bind(AUTH_SYMBOLS.IAuthenticationService) + .toConstantValue(authService); + }); + + it("returns a cookie on valid credentials", async () => { + const cookie = await signInController({ + username: "alice", + password: "password_alice", + }); + expect(cookie.name).toBe("session"); + }); + + it("throws InputParseError on missing username", async () => { + await expect( + signInController({ password: "anything" }), + ).rejects.toBeInstanceOf(InputParseError); + }); + + it("throws InputParseError on too-short password", async () => { + await expect( + signInController({ username: "alice", password: "abc" }), + ).rejects.toBeInstanceOf(InputParseError); + }); +}); diff --git a/packages/auth/src/interface-adapters/controllers/sign-in.controller.ts b/packages/auth/src/interface-adapters/controllers/sign-in.controller.ts new file mode 100644 index 0000000..a6fd143 --- /dev/null +++ b/packages/auth/src/interface-adapters/controllers/sign-in.controller.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +import { InputParseError } from "../../entities/errors"; +import type { Cookie } from "../../entities/cookie"; +import { signInUseCase } from "../../application/use-cases/sign-in.use-case"; + +const inputSchema = z.object({ + username: z.string().min(3).max(31), + password: z.string().min(6).max(255), +}); + +export async function signInController( + input: Partial>, +): Promise { + const parsed = inputSchema.safeParse(input); + if (!parsed.success) { + throw new InputParseError("Invalid sign-in input", { cause: parsed.error }); + } + const { cookie } = await signInUseCase(parsed.data); + return cookie; +} diff --git a/packages/auth/src/interface-adapters/controllers/sign-out.controller.test.ts b/packages/auth/src/interface-adapters/controllers/sign-out.controller.test.ts new file mode 100644 index 0000000..29dd561 --- /dev/null +++ b/packages/auth/src/interface-adapters/controllers/sign-out.controller.test.ts @@ -0,0 +1,40 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { authContainer } from "@/di/container"; +import { AUTH_SYMBOLS } from "@/di/symbols"; +import { MockUsersRepository } from "@/infrastructure/repositories/mock-users.repository"; +import { MockAuthenticationService } from "@/infrastructure/services/mock-authentication.service"; +import type { IUsersRepository } from "@/application/repositories/users-repository.interface"; +import type { IAuthenticationService } from "@/application/services/authentication-service.interface"; +import { InputParseError } from "@/entities/errors"; +import { signOutController } from "./sign-out.controller"; + +describe("signOutController", () => { + beforeEach(() => { + if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) { + authContainer.unbind(AUTH_SYMBOLS.IUsersRepository); + } + if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) { + authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService); + } + const usersRepo = new MockUsersRepository(); + const authService = new MockAuthenticationService(usersRepo); + authContainer + .bind(AUTH_SYMBOLS.IUsersRepository) + .toConstantValue(usersRepo); + authContainer + .bind(AUTH_SYMBOLS.IAuthenticationService) + .toConstantValue(authService); + }); + + it("returns a blank cookie", async () => { + const cookie = await signOutController("session_anything"); + expect(cookie.name).toBe("session"); + expect(cookie.value).toBe(""); + }); + + it("throws InputParseError when sessionId is missing", async () => { + await expect(signOutController(undefined)).rejects.toBeInstanceOf( + InputParseError, + ); + }); +}); diff --git a/packages/auth/src/interface-adapters/controllers/sign-out.controller.ts b/packages/auth/src/interface-adapters/controllers/sign-out.controller.ts new file mode 100644 index 0000000..7017f57 --- /dev/null +++ b/packages/auth/src/interface-adapters/controllers/sign-out.controller.ts @@ -0,0 +1,13 @@ +import { InputParseError } from "../../entities/errors"; +import type { Cookie } from "../../entities/cookie"; +import { signOutUseCase } from "../../application/use-cases/sign-out.use-case"; + +export async function signOutController( + sessionId: string | undefined, +): Promise { + if (!sessionId) { + throw new InputParseError("Must provide a session ID"); + } + const { blankCookie } = await signOutUseCase(sessionId); + return blankCookie; +} diff --git a/packages/auth/src/interface-adapters/controllers/sign-up.controller.test.ts b/packages/auth/src/interface-adapters/controllers/sign-up.controller.test.ts new file mode 100644 index 0000000..04d2e45 --- /dev/null +++ b/packages/auth/src/interface-adapters/controllers/sign-up.controller.test.ts @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { authContainer } from "@/di/container"; +import { AUTH_SYMBOLS } from "@/di/symbols"; +import { MockUsersRepository } from "@/infrastructure/repositories/mock-users.repository"; +import { MockAuthenticationService } from "@/infrastructure/services/mock-authentication.service"; +import type { IUsersRepository } from "@/application/repositories/users-repository.interface"; +import type { IAuthenticationService } from "@/application/services/authentication-service.interface"; +import { InputParseError } from "@/entities/errors"; +import { signUpController } from "./sign-up.controller"; + +describe("signUpController", () => { + let usersRepo: MockUsersRepository; + let authService: MockAuthenticationService; + + beforeEach(() => { + if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) { + authContainer.unbind(AUTH_SYMBOLS.IUsersRepository); + } + if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) { + authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService); + } + usersRepo = new MockUsersRepository(); + authService = new MockAuthenticationService(usersRepo); + authContainer + .bind(AUTH_SYMBOLS.IUsersRepository) + .toConstantValue(usersRepo); + authContainer + .bind(AUTH_SYMBOLS.IAuthenticationService) + .toConstantValue(authService); + }); + + it("creates a new user when passwords match", async () => { + const result = await signUpController({ + username: "carol", + password: "secret_password", + confirmPassword: "secret_password", + }); + expect(result.user.username).toBe("carol"); + }); + + it("throws InputParseError when passwords do not match", async () => { + await expect( + signUpController({ + username: "dave", + password: "secret_password", + confirmPassword: "different_password", + }), + ).rejects.toBeInstanceOf(InputParseError); + }); +}); diff --git a/packages/auth/src/interface-adapters/controllers/sign-up.controller.ts b/packages/auth/src/interface-adapters/controllers/sign-up.controller.ts new file mode 100644 index 0000000..8455cd4 --- /dev/null +++ b/packages/auth/src/interface-adapters/controllers/sign-up.controller.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +import { InputParseError } from "../../entities/errors"; +import { signUpUseCase } from "../../application/use-cases/sign-up.use-case"; + +const inputSchema = z + .object({ + username: z.string().min(3).max(31), + password: z.string().min(6).max(255), + confirmPassword: z.string().min(6).max(255), + }) + .superRefine(({ password, confirmPassword }, ctx) => { + if (confirmPassword !== password) { + ctx.addIssue({ + code: "custom", + message: "The passwords did not match", + path: ["password"], + }); + ctx.addIssue({ + code: "custom", + message: "The passwords did not match", + path: ["confirmPassword"], + }); + } + }); + +export async function signUpController( + input: Partial>, +): Promise> { + const parsed = inputSchema.safeParse(input); + if (!parsed.success) { + throw new InputParseError("Invalid sign-up input", { cause: parsed.error }); + } + return await signUpUseCase(parsed.data); +}