feat(core): add auth use cases with tests (sign-in, sign-up, sign-out)
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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("");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user