feat(core): add auth use cases with tests (sign-in, sign-up, sign-out)

This commit is contained in:
2026-04-06 14:27:23 +02:00
parent c73d4acec9
commit 45cb0ee972
6 changed files with 176 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<User, "id" | "username">;
}> {
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 },
};
}

View File

@@ -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);
});
});

View File

@@ -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("");
});
});

View File

@@ -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);
});
});