feat: add accounts and movies tables in migrations

- Created migration for accounts table with fields: id, username, password, role_id, created_at, updated_at.
- Created migration for movies table with fields: id, title, overview, poster_path, backdrop_path, release_date, tmdb_id.

refactor: update package.json scripts and dependencies

- Changed dev script to use bun instead of tsx.
- Added build:migrate script for migration.
- Updated devDependencies for bun and oxlint.

fix: refactor database connection and migration execution

- Updated PgDatabase to use SQL from bun.
- Refactored migration execution logic to read SQL files and execute them.

feat: implement account creation and validation logic

- Updated AccountEntity to use username instead of email.
- Added validation for username format and password strength.
- Implemented account repository methods for finding by username and inserting accounts.

test: add tests for account entity, repository, and service

- Created tests for AccountEntity to validate username and password.
- Added tests for AccountRepository to ensure correct database interactions.
- Implemented tests for AccountService to validate registration and login logic.

chore: remove outdated tests and files

- Deleted old tests related to email-based account management.
- Cleaned up unused imports and files to streamline the codebase.
This commit is contained in:
2025-10-02 20:54:41 +00:00
parent 91c14f750e
commit aaa0ca5a54
28 changed files with 617 additions and 654 deletions

View File

@@ -0,0 +1,80 @@
import { describe, it, expect } from "bun:test";
import {
InvalidUsernameFormatError,
WeakPasswordError,
} from "../../src/domain/account/errors/AccountErrors";
import {
MAX_USERNAME_LENGTH,
MIN_PASSWORD_LENGTH,
} from "../../src/domain/account/validation/AccountValidation";
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
describe("AccountEntity", () => {
describe("create", () => {
it("should create an account with valid username and password", async () => {
const email = "testaccount";
const password = "a".repeat(MIN_PASSWORD_LENGTH);
const account = await AccountEntity.create(email, password);
expect(account.username).toBe(email);
expect(account.roleId).toBe(1);
expect(account).toBeDefined();
});
it("should throw InvalidUsernameFormatError for invalid username", () => {
const invalidUsername = "a".repeat(MAX_USERNAME_LENGTH + 1);
const password = "a".repeat(MIN_PASSWORD_LENGTH);
expect(AccountEntity.create(invalidUsername, password)).rejects.toThrow(
InvalidUsernameFormatError,
);
});
it("should throw WeakPasswordError for short password", () => {
const username = "testaccount";
const shortPassword = "a".repeat(MIN_PASSWORD_LENGTH - 1);
expect(AccountEntity.create(username, shortPassword)).rejects.toThrow(
WeakPasswordError,
);
});
});
describe("verifyPassword", () => {
it("should return true for correct password", async () => {
const createAccount = await AccountEntity.create(
"test@example.com",
"password123",
);
const account = new AccountEntity(
1,
createAccount.username,
createAccount.hashedPassword,
createAccount.roleId,
new Date(),
new Date(),
);
expect(await account.verifyPassword("password123")).toBe(true);
});
it("should return false for incorrect password", async () => {
const createAccount = await AccountEntity.create(
"test@example.com",
"password123",
);
const account = new AccountEntity(
1,
createAccount.username,
createAccount.hashedPassword,
createAccount.roleId,
new Date(),
new Date(),
);
expect(await account.verifyPassword("wrongpassword")).toBe(false);
});
});
});

View File

@@ -0,0 +1,74 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import AccountRepository from "../../src/domain/account/repository/AccoutRepository";
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
import { initTestDatabase } from "../utils";
import type { DatabaseInterface } from "../../src/database/DatabaseInterface";
describe("AccountRepository", () => {
let accountRepository: AccountRepository;
let database: DatabaseInterface;
beforeEach(async () => {
database = await initTestDatabase();
accountRepository = new AccountRepository(database);
});
afterEach(async () => {
await database?.close();
});
describe("findByUsername", () => {
it("should return account when found", async () => {
const username = "testaccount";
const createAccount = await AccountEntity.create(username, "password123");
await accountRepository.insert(createAccount);
const result = await accountRepository.findByUsername(username);
expect(result).toBeInstanceOf(AccountEntity);
expect(result?.username).toBe(username);
});
it("should return null when account not found", async () => {
const username = "nonexistentaccount";
const result = await accountRepository.findByUsername(username);
expect(result).toBeNull();
});
});
describe("findById", () => {
it("should return account when found", async () => {
const createAccount = await AccountEntity.create(
"test@example.com",
"password123",
);
const newId = await accountRepository.insert(createAccount);
const result = await accountRepository.findById(newId);
expect(result).toBeInstanceOf(AccountEntity);
expect(result?.id).toBe(newId);
});
it("should return null when account not found", async () => {
const id = 999;
const result = await accountRepository.findById(id);
expect(result).toBeNull();
});
});
describe("insert", () => {
it("should insert account successfully", async () => {
const account = await AccountEntity.create(
"test@example.com",
"password123",
);
const result = await accountRepository.insert(account);
expect(result).toBeGreaterThan(0);
});
});
});

View File

@@ -0,0 +1,145 @@
import { describe, it, expect, jest, beforeEach } from "bun:test";
import {
AccountNotFoundError,
AccountAlreadyExistsError,
BadPasswordError,
} from "../../src/domain/account/errors/AccountErrors";
import AccountService from "../../src/domain/account/service/AccountService";
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
describe("AccountService", () => {
let accountService: AccountService;
let mockAccountRepository: {
findByUsername: ReturnType<typeof jest.fn>;
insert: ReturnType<typeof jest.fn>;
findById: ReturnType<typeof jest.fn>;
};
beforeEach(() => {
mockAccountRepository = {
findByUsername: jest.fn(() => Promise.resolve(null)),
insert: jest.fn(() => Promise.resolve("123")),
findById: jest.fn(() => Promise.resolve(null)),
};
accountService = new AccountService(mockAccountRepository as any);
});
describe("createAccount", () => {
it("should create a new account successfully", async () => {
const username = "testusername";
const password = "password123";
mockAccountRepository.findByUsername.mockImplementation(() =>
Promise.resolve(null),
);
mockAccountRepository.findById.mockImplementation(() =>
Promise.resolve(
new AccountEntity(
123,
username,
"hashedpassword",
1,
new Date(),
new Date(),
),
),
);
mockAccountRepository.insert.mockImplementation(() =>
Promise.resolve(123),
);
const result = await accountService.register(username, password);
expect(result).toBeInstanceOf(AccountEntity);
expect(result.username).toBe(username);
expect(mockAccountRepository.findByUsername).toHaveBeenCalledWith(
username,
);
});
it("should throw error if account already exists", async () => {
const username = "testusername";
const password = "password123";
const existingAccount = await AccountEntity.create(username, password);
mockAccountRepository.findByUsername.mockImplementation(() =>
Promise.resolve(existingAccount),
);
expect(accountService.register(username, password)).rejects.toThrow(
AccountAlreadyExistsError,
);
});
});
describe("login", () => {
it("should login successfully with correct credentials", async () => {
const username = "testaccount";
const password = "password123";
const createAccount = await AccountEntity.create(username, password);
const account = new AccountEntity(
1,
createAccount.username,
createAccount.hashedPassword,
createAccount.roleId,
new Date(),
new Date(),
);
mockAccountRepository.findByUsername.mockImplementation(() =>
Promise.resolve(account),
);
const result = await accountService.login(username, password);
expect({ username: result.username, roleId: result.roleId }).toEqual({
username: account.username,
roleId: account.roleId,
});
expect(mockAccountRepository.findByUsername).toHaveBeenCalledWith(
username,
);
});
it("should throw error if account not found", async () => {
const username = "testaccount";
const password = "password123";
mockAccountRepository.findByUsername.mockImplementation(() =>
Promise.resolve(null),
);
expect(accountService.login(username, password)).rejects.toThrow(
AccountNotFoundError,
);
});
it("should throw error if password is incorrect", async () => {
const username = "testaccount";
const password = "password123";
const wrongPassword = "wrongpassword";
const createAccount = await AccountEntity.create(username, password);
const account = new AccountEntity(
1,
createAccount.username,
createAccount.hashedPassword,
createAccount.roleId,
new Date(),
new Date(),
);
mockAccountRepository.findByUsername.mockImplementation(() =>
Promise.resolve(account),
);
expect(accountService.login(username, wrongPassword)).rejects.toThrow(
BadPasswordError,
);
});
});
});

View File

@@ -0,0 +1,93 @@
import { describe, it, expect } from "bun:test";
import {
passwordSchema,
loginSchema,
registerSchema,
MIN_PASSWORD_LENGTH,
} from "../../src/domain/account/validation/AccountValidation";
import { WeakPasswordError } from "../../src/domain/account/errors/AccountErrors";
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
describe("AccountValidation", () => {
describe("Password validation consistency", () => {
const validPasswords = ["password123", "abcdefgh", "12345678", "P@ssw0rd!"];
const invalidPasswords = ["1234567", "short", "", "abc"];
it("should validate same passwords in Zod and Entity", () => {
validPasswords.forEach((password) => {
expect(passwordSchema.safeParse(password).success).toBe(true);
expect(() =>
AccountEntity.create("test@example.com", password),
).not.toThrow(WeakPasswordError);
});
invalidPasswords.forEach((password) => {
expect(passwordSchema.safeParse(password).success).toBe(false);
expect(() =>
AccountEntity.create("test@example.com", password),
).toThrow(WeakPasswordError);
});
});
it("should use same minimum length", () => {
validPasswords.forEach((password) => {
expect(password.length >= MIN_PASSWORD_LENGTH).toBe(true);
});
invalidPasswords.forEach((password) => {
expect(password.length >= MIN_PASSWORD_LENGTH).toBe(false);
});
});
});
describe("Complete schemas", () => {
it("should validate login schema correctly", () => {
const validLogin = {
username: "testaccount",
password: "password123",
};
const invalidLogin = { username: "ii", password: "123" };
expect(loginSchema.safeParse(validLogin).success).toBe(true);
expect(loginSchema.safeParse(invalidLogin).success).toBe(false);
});
it("should validate register schema correctly", () => {
const validRegister = {
username: "testaccount",
password: "password123",
};
const invalidRegister = { username: "ii", password: "123" };
expect(registerSchema.safeParse(validRegister).success).toBe(true);
expect(registerSchema.safeParse(invalidRegister).success).toBe(false);
});
});
describe("Error messages consistency", () => {
it("should return consistent password error message", () => {
const result = passwordSchema.safeParse("123");
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain(
`${MIN_PASSWORD_LENGTH} caractères`,
);
}
});
it("should return consistent username error message", () => {
const result = registerSchema.safeParse({
username: "ii",
password: "validPassword123",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toContain(
"doit contenir au moins",
);
}
});
});
});