diff --git a/packages/core/src/interface-adapters/controllers/auth/sign-in.controller.ts b/packages/core/src/interface-adapters/controllers/auth/sign-in.controller.ts new file mode 100644 index 0000000..745a7c2 --- /dev/null +++ b/packages/core/src/interface-adapters/controllers/auth/sign-in.controller.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +import { InputParseError } from "@/entities/errors/common.js"; +import type { Cookie } from "@/entities/models/cookie.js"; +import { signInUseCase } from "@/application/use-cases/auth/sign-in.use-case.js"; + +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 { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + const { cookie } = await signInUseCase(data); + return cookie; +} diff --git a/packages/core/src/interface-adapters/controllers/auth/sign-out.controller.ts b/packages/core/src/interface-adapters/controllers/auth/sign-out.controller.ts new file mode 100644 index 0000000..ed75dfe --- /dev/null +++ b/packages/core/src/interface-adapters/controllers/auth/sign-out.controller.ts @@ -0,0 +1,14 @@ +import { InputParseError } from "@/entities/errors/common.js"; +import type { Cookie } from "@/entities/models/cookie.js"; +import { signOutUseCase } from "@/application/use-cases/auth/sign-out.use-case.js"; + +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/core/src/interface-adapters/controllers/auth/sign-up.controller.ts b/packages/core/src/interface-adapters/controllers/auth/sign-up.controller.ts new file mode 100644 index 0000000..5cad5b4 --- /dev/null +++ b/packages/core/src/interface-adapters/controllers/auth/sign-up.controller.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +import { InputParseError } from "@/entities/errors/common.js"; +import { signUpUseCase } from "@/application/use-cases/auth/sign-up.use-case.js"; + +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 { data, error: inputParseError } = inputSchema.safeParse(input); + + if (inputParseError) { + throw new InputParseError("Invalid data", { cause: inputParseError }); + } + + return await signUpUseCase(data); +} diff --git a/packages/core/tests/unit/controllers/auth/sign-in.controller.test.ts b/packages/core/tests/unit/controllers/auth/sign-in.controller.test.ts new file mode 100644 index 0000000..1de30ad --- /dev/null +++ b/packages/core/tests/unit/controllers/auth/sign-in.controller.test.ts @@ -0,0 +1,41 @@ +import "reflect-metadata"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + destroyContainer, + initializeContainer, +} from "@/di/container.js"; +import { signInController } from "@/interface-adapters/controllers/auth/sign-in.controller.js"; +import { InputParseError } from "@/entities/errors/common.js"; +import { AuthenticationError } from "@/entities/errors/auth.js"; + +beforeEach(() => { + initializeContainer(); +}); + +afterEach(() => { + destroyContainer(); +}); + +describe("signInController", () => { + it("returns cookie for valid input", async () => { + const cookie = await signInController({ + username: "alice", + password: "password_alice", + }); + expect(cookie).toHaveProperty("name"); + expect(cookie).toHaveProperty("value"); + }); + + it("throws InputParseError for invalid input", async () => { + await expect( + signInController({ username: "ab", password: "short" }) + ).rejects.toBeInstanceOf(InputParseError); + }); + + it("throws AuthenticationError for wrong credentials", async () => { + await expect( + signInController({ username: "alice", password: "wrongpassword" }) + ).rejects.toBeInstanceOf(AuthenticationError); + }); +}); diff --git a/packages/core/tests/unit/controllers/auth/sign-out.controller.test.ts b/packages/core/tests/unit/controllers/auth/sign-out.controller.test.ts new file mode 100644 index 0000000..f631ebd --- /dev/null +++ b/packages/core/tests/unit/controllers/auth/sign-out.controller.test.ts @@ -0,0 +1,30 @@ +import "reflect-metadata"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + destroyContainer, + initializeContainer, +} from "@/di/container.js"; +import { signOutController } from "@/interface-adapters/controllers/auth/sign-out.controller.js"; +import { InputParseError } from "@/entities/errors/common.js"; + +beforeEach(() => { + initializeContainer(); +}); + +afterEach(() => { + destroyContainer(); +}); + +describe("signOutController", () => { + it("returns blank cookie for valid session", async () => { + const cookie = await signOutController("some-session-id"); + expect(cookie.value).toBe(""); + }); + + it("throws InputParseError when no session ID provided", async () => { + await expect(signOutController(undefined)).rejects.toBeInstanceOf( + InputParseError + ); + }); +}); diff --git a/packages/core/tests/unit/controllers/auth/sign-up.controller.test.ts b/packages/core/tests/unit/controllers/auth/sign-up.controller.test.ts new file mode 100644 index 0000000..ae97f0b --- /dev/null +++ b/packages/core/tests/unit/controllers/auth/sign-up.controller.test.ts @@ -0,0 +1,44 @@ +import "reflect-metadata"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + destroyContainer, + initializeContainer, +} from "@/di/container.js"; +import { signUpController } from "@/interface-adapters/controllers/auth/sign-up.controller.js"; +import { InputParseError } from "@/entities/errors/common.js"; + +beforeEach(() => { + initializeContainer(); +}); + +afterEach(() => { + destroyContainer(); +}); + +describe("signUpController", () => { + it("returns session, cookie, and user for valid input", async () => { + const result = await signUpController({ + username: "newuser", + password: "securepassword", + confirmPassword: "securepassword", + }); + expect(result).toHaveProperty("session"); + expect(result).toHaveProperty("cookie"); + expect(result).toHaveProperty("user"); + }); + + it("throws InputParseError when passwords don't match", async () => { + await expect( + signUpController({ + username: "newuser", + password: "password1", + confirmPassword: "password2", + }) + ).rejects.toBeInstanceOf(InputParseError); + }); + + it("throws InputParseError for missing fields", async () => { + await expect(signUpController({})).rejects.toBeInstanceOf(InputParseError); + }); +});