From 45cb0ee972dbfb81cb8daaf20faf2756c4838a7e Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 6 Apr 2026 14:27:23 +0200 Subject: [PATCH] feat(core): add auth use cases with tests (sign-in, sign-up, sign-out) --- .../use-cases/auth/sign-in.use-case.ts | 27 ++++++++++++ .../use-cases/auth/sign-out.use-case.ts | 9 ++++ .../use-cases/auth/sign-up.use-case.ts | 39 ++++++++++++++++++ .../use-cases/auth/sign-in.use-case.test.ts | 41 +++++++++++++++++++ .../use-cases/auth/sign-out.use-case.test.ts | 24 +++++++++++ .../use-cases/auth/sign-up.use-case.test.ts | 36 ++++++++++++++++ 6 files changed, 176 insertions(+) create mode 100644 packages/core/src/application/use-cases/auth/sign-in.use-case.ts create mode 100644 packages/core/src/application/use-cases/auth/sign-out.use-case.ts create mode 100644 packages/core/src/application/use-cases/auth/sign-up.use-case.ts create mode 100644 packages/core/tests/unit/use-cases/auth/sign-in.use-case.test.ts create mode 100644 packages/core/tests/unit/use-cases/auth/sign-out.use-case.test.ts create mode 100644 packages/core/tests/unit/use-cases/auth/sign-up.use-case.test.ts diff --git a/packages/core/src/application/use-cases/auth/sign-in.use-case.ts b/packages/core/src/application/use-cases/auth/sign-in.use-case.ts new file mode 100644 index 0000000..eb4a9b1 --- /dev/null +++ b/packages/core/src/application/use-cases/auth/sign-in.use-case.ts @@ -0,0 +1,27 @@ +import { AuthenticationError } from "@/entities/errors/auth.js"; +import type { Cookie } from "@/entities/models/cookie.js"; +import type { Session } from "@/entities/models/session.js"; +import { getInjection } from "@/di/container.js"; + +export async function signInUseCase(input: { + username: string; + password: string; +}): Promise<{ session: Session; cookie: Cookie }> { + const usersRepository = getInjection("IUsersRepository"); + const authService = getInjection("IAuthenticationService"); + + const existingUser = await usersRepository.getUserByUsername(input.username); + if (!existingUser) { + throw new AuthenticationError("User does not exist"); + } + + const validPassword = await authService.verifyPassword( + existingUser.passwordHash, + input.password + ); + if (!validPassword) { + throw new AuthenticationError("Incorrect username or password"); + } + + return await authService.createSession(existingUser); +} diff --git a/packages/core/src/application/use-cases/auth/sign-out.use-case.ts b/packages/core/src/application/use-cases/auth/sign-out.use-case.ts new file mode 100644 index 0000000..e07c2a7 --- /dev/null +++ b/packages/core/src/application/use-cases/auth/sign-out.use-case.ts @@ -0,0 +1,9 @@ +import type { Cookie } from "@/entities/models/cookie.js"; +import { getInjection } from "@/di/container.js"; + +export async function signOutUseCase( + sessionId: string +): Promise<{ blankCookie: Cookie }> { + const authService = getInjection("IAuthenticationService"); + return await authService.invalidateSession(sessionId); +} diff --git a/packages/core/src/application/use-cases/auth/sign-up.use-case.ts b/packages/core/src/application/use-cases/auth/sign-up.use-case.ts new file mode 100644 index 0000000..f264ea9 --- /dev/null +++ b/packages/core/src/application/use-cases/auth/sign-up.use-case.ts @@ -0,0 +1,39 @@ +import { AuthenticationError } from "@/entities/errors/auth.js"; +import type { Cookie } from "@/entities/models/cookie.js"; +import type { Session } from "@/entities/models/session.js"; +import type { User } from "@/entities/models/user.js"; +import { getInjection } from "@/di/container.js"; + +export async function signUpUseCase(input: { + username: string; + password: string; +}): Promise<{ + session: Session; + cookie: Cookie; + user: Pick; +}> { + const usersRepository = getInjection("IUsersRepository"); + const authService = getInjection("IAuthenticationService"); + + const existingUser = await usersRepository.getUserByUsername(input.username); + if (existingUser) { + throw new AuthenticationError("Username taken"); + } + + const passwordHash = await authService.hashPassword(input.password); + const userId = authService.generateUserId(); + + const newUser = await usersRepository.createUser({ + id: userId, + username: input.username, + passwordHash, + }); + + const { cookie, session } = await authService.createSession(newUser); + + return { + cookie, + session, + user: { id: newUser.id, username: newUser.username }, + }; +} diff --git a/packages/core/tests/unit/use-cases/auth/sign-in.use-case.test.ts b/packages/core/tests/unit/use-cases/auth/sign-in.use-case.test.ts new file mode 100644 index 0000000..b515721 --- /dev/null +++ b/packages/core/tests/unit/use-cases/auth/sign-in.use-case.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 { signInUseCase } from "@/application/use-cases/auth/sign-in.use-case.js"; +import { AuthenticationError } from "@/entities/errors/auth.js"; + +beforeEach(() => { + initializeContainer(); +}); + +afterEach(() => { + destroyContainer(); +}); + +describe("signInUseCase", () => { + it("returns session and cookie for valid credentials", async () => { + const result = await signInUseCase({ + username: "alice", + password: "password_alice", + }); + expect(result).toHaveProperty("session"); + expect(result).toHaveProperty("cookie"); + expect(result.session.userId).toBe("1"); + }); + + it("throws AuthenticationError for non-existing user", async () => { + await expect( + signInUseCase({ username: "non-existing", password: "any" }) + ).rejects.toBeInstanceOf(AuthenticationError); + }); + + it("throws AuthenticationError for wrong password", async () => { + await expect( + signInUseCase({ username: "alice", password: "wrong" }) + ).rejects.toBeInstanceOf(AuthenticationError); + }); +}); diff --git a/packages/core/tests/unit/use-cases/auth/sign-out.use-case.test.ts b/packages/core/tests/unit/use-cases/auth/sign-out.use-case.test.ts new file mode 100644 index 0000000..0bc78d4 --- /dev/null +++ b/packages/core/tests/unit/use-cases/auth/sign-out.use-case.test.ts @@ -0,0 +1,24 @@ +import "reflect-metadata"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + destroyContainer, + initializeContainer, +} from "@/di/container.js"; +import { signOutUseCase } from "@/application/use-cases/auth/sign-out.use-case.js"; + +beforeEach(() => { + initializeContainer(); +}); + +afterEach(() => { + destroyContainer(); +}); + +describe("signOutUseCase", () => { + it("returns a blank cookie", async () => { + const result = await signOutUseCase("some-session-id"); + expect(result).toHaveProperty("blankCookie"); + expect(result.blankCookie.value).toBe(""); + }); +}); diff --git a/packages/core/tests/unit/use-cases/auth/sign-up.use-case.test.ts b/packages/core/tests/unit/use-cases/auth/sign-up.use-case.test.ts new file mode 100644 index 0000000..02aba06 --- /dev/null +++ b/packages/core/tests/unit/use-cases/auth/sign-up.use-case.test.ts @@ -0,0 +1,36 @@ +import "reflect-metadata"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + destroyContainer, + initializeContainer, +} from "@/di/container.js"; +import { signUpUseCase } from "@/application/use-cases/auth/sign-up.use-case.js"; +import { AuthenticationError } from "@/entities/errors/auth.js"; + +beforeEach(() => { + initializeContainer(); +}); + +afterEach(() => { + destroyContainer(); +}); + +describe("signUpUseCase", () => { + it("creates user and returns session, cookie, and user info", async () => { + const result = await signUpUseCase({ + username: "newuser", + password: "securepassword", + }); + expect(result).toHaveProperty("session"); + expect(result).toHaveProperty("cookie"); + expect(result).toHaveProperty("user"); + expect(result.user.username).toBe("newuser"); + }); + + it("throws AuthenticationError if username is taken", async () => { + await expect( + signUpUseCase({ username: "alice", password: "anypassword" }) + ).rejects.toBeInstanceOf(AuthenticationError); + }); +});