Ajout de la gestion des erreurs HTTP, implémentation de la validation des comptes, et mise à jour des dépendances. Création de tests pour les entités et services de compte, ainsi que l'ajout d'un système de limitation de taux.

This commit is contained in:
2025-08-12 18:29:05 +00:00
parent 14e69e1f61
commit 3fe9fc7142
18 changed files with 1774 additions and 64 deletions

View File

@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
import {
InvalidEmailFormatError,
WeakPasswordError,
} from "../src/domain/account/errors/AccountErrors";
import { MIN_PASSWORD_LENGTH } from "../src/domain/account/validation/AccountValidation";
describe("AccountEntity", () => {
describe("create", () => {
it("should create an account with valid email and password", () => {
const email = "test@example.com";
const password = "a".repeat(MIN_PASSWORD_LENGTH); // Use minimum length
const account = AccountEntity.create(email, password);
expect(account.email).toBe(email);
expect(account.roleId).toBe(1);
expect(account.id).toBeDefined();
expect(account.createdAt).toBeInstanceOf(Date);
expect(account.updatedAt).toBeInstanceOf(Date);
});
it("should throw InvalidEmailFormatError for invalid email", () => {
const invalidEmail = "invalid-email";
const password = "password123";
expect(() => {
AccountEntity.create(invalidEmail, password);
}).toThrow(InvalidEmailFormatError);
});
it("should throw WeakPasswordError for short password", () => {
const email = "test@example.com";
const shortPassword = "a".repeat(MIN_PASSWORD_LENGTH - 1); // One less than minimum
expect(() => {
AccountEntity.create(email, shortPassword);
}).toThrow(WeakPasswordError);
});
});
describe("verifyPassword", () => {
it("should return true for correct password", () => {
const account = AccountEntity.create("test@example.com", "password123");
expect(account.verifyPassword("password123")).toBe(true);
});
it("should return false for incorrect password", () => {
const account = AccountEntity.create("test@example.com", "password123");
expect(account.verifyPassword("wrongpassword")).toBe(false);
});
});
});

View File

@ -0,0 +1,116 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import AccountRepository from "../src/domain/account/repository/AccoutRepository";
import { DatabaseInterface } from "../src/database/DatabaseInterface";
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
describe("AccountRepository", () => {
let accountRepository: AccountRepository;
let mockDatabase: DatabaseInterface;
beforeEach(() => {
mockDatabase = {
ping: vi.fn(),
fetchAll: vi.fn(),
fetchOne: vi.fn(),
execute: vi.fn(),
};
accountRepository = new AccountRepository(mockDatabase);
});
describe("findByEmail", () => {
it("should return account when found", async () => {
const email = "test@example.com";
const mockResult = {
id: "123",
email: email,
password: "hashedPassword",
role_id: 1,
created_at: new Date(),
updated_at: new Date(),
};
vi.mocked(mockDatabase.fetchOne).mockResolvedValue(mockResult);
const result = await accountRepository.findByEmail(email);
expect(result).toBeInstanceOf(AccountEntity);
expect(result?.email).toBe(email);
expect(mockDatabase.fetchOne).toHaveBeenCalledWith(
expect.stringContaining(
"SELECT id, email, password, role_id, created_at, updated_at",
),
[email],
);
});
it("should return null when account not found", async () => {
const email = "nonexistent@example.com";
vi.mocked(mockDatabase.fetchOne).mockResolvedValue(undefined);
const result = await accountRepository.findByEmail(email);
expect(result).toBeNull();
});
});
describe("findById", () => {
it("should return account when found", async () => {
const id = "123";
const mockResult = {
id: id,
email: "test@example.com",
password: "hashedPassword",
role_id: 1,
created_at: new Date(),
updated_at: new Date(),
};
vi.mocked(mockDatabase.fetchOne).mockResolvedValue(mockResult);
const result = await accountRepository.findById(id);
expect(result).toBeInstanceOf(AccountEntity);
expect(result?.id).toBe(id);
expect(mockDatabase.fetchOne).toHaveBeenCalledWith(
expect.stringContaining(
"SELECT id, email, password, role_id, created_at, updated_at",
),
[id],
);
});
it("should return null when account not found", async () => {
const id = "nonexistent";
vi.mocked(mockDatabase.fetchOne).mockResolvedValue(undefined);
const result = await accountRepository.findById(id);
expect(result).toBeNull();
});
});
describe("save", () => {
it("should save account successfully", async () => {
const account = AccountEntity.create("test@example.com", "password123");
vi.mocked(mockDatabase.fetchOne).mockResolvedValue({ id: account.id });
const result = await accountRepository.save(account);
expect(result).toBe(account.id);
expect(mockDatabase.fetchOne).toHaveBeenCalledWith(
expect.stringContaining("INSERT INTO accounts"),
expect.arrayContaining([
account.id,
account.email,
expect.any(String),
account.roleId,
account.createdAt,
account.updatedAt,
]),
);
});
});
});

View File

@ -0,0 +1,95 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import AccountService from "../src/domain/account/service/AccountService";
import { AccountRepositoryInterface } from "../src/domain/account/repository/AccountRepositoryInterface";
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
import {
AccountNotFoundError,
AccountAlreadyExistsError,
BadPasswordError,
} from "../src/domain/account/errors/AccountErrors";
describe("AccountService", () => {
let accountService: AccountService;
let mockAccountRepository: AccountRepositoryInterface;
beforeEach(() => {
mockAccountRepository = {
findByEmail: vi.fn(),
save: vi.fn(),
findById: vi.fn(),
};
accountService = new AccountService(mockAccountRepository);
});
describe("createAccount", () => {
it("should create a new account successfully", async () => {
const email = "test@example.com";
const password = "password123";
vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(null);
vi.mocked(mockAccountRepository.save).mockResolvedValue("123");
const result = await accountService.register(email, password);
expect(result).toBeInstanceOf(AccountEntity);
expect(result.email).toBe(email);
expect(mockAccountRepository.findByEmail).toHaveBeenCalledWith(email);
expect(mockAccountRepository.save).toHaveBeenCalledWith(
expect.any(AccountEntity),
);
});
it("should throw error if account already exists", async () => {
const email = "test@example.com";
const password = "password123";
const existingAccount = AccountEntity.create(email, password);
vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(
existingAccount,
);
await expect(accountService.register(email, password)).rejects.toThrow(
AccountAlreadyExistsError,
);
});
});
describe("login", () => {
it("should login successfully with correct credentials", async () => {
const email = "test@example.com";
const password = "password123";
const account = AccountEntity.create(email, password);
vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(account);
const result = await accountService.login(email, password);
expect(result).toBe(account);
expect(mockAccountRepository.findByEmail).toHaveBeenCalledWith(email);
});
it("should throw error if account not found", async () => {
const email = "test@example.com";
const password = "password123";
vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(null);
await expect(accountService.login(email, password)).rejects.toThrow(
AccountNotFoundError,
);
});
it("should throw error if password is incorrect", async () => {
const email = "test@example.com";
const password = "password123";
const wrongPassword = "wrongpassword";
const account = AccountEntity.create(email, password);
vi.mocked(mockAccountRepository.findByEmail).mockResolvedValue(account);
await expect(accountService.login(email, wrongPassword)).rejects.toThrow(
BadPasswordError,
);
});
});
});

View File

@ -0,0 +1,132 @@
import { describe, it, expect } from "vitest";
import {
emailSchema,
passwordSchema,
loginSchema,
registerSchema,
EMAIL_REGEX,
MIN_PASSWORD_LENGTH,
} from "../src/domain/account/validation/AccountValidation";
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
import {
InvalidEmailFormatError,
WeakPasswordError,
} from "../src/domain/account/errors/AccountErrors";
describe("AccountValidation", () => {
describe("Email validation consistency", () => {
const validEmails = [
"test@example.com",
"user.name@domain.co.uk",
"firstname+lastname@example.org",
];
const invalidEmails = [
"invalid-email",
"@example.com",
"test@",
"test",
"test@domain",
"",
];
it("should validate same emails in Zod and Entity", () => {
validEmails.forEach((email) => {
expect(emailSchema.safeParse(email).success).toBe(true);
expect(() => AccountEntity.create(email, "password123")).not.toThrow(
InvalidEmailFormatError,
);
});
invalidEmails.forEach((email) => {
expect(emailSchema.safeParse(email).success).toBe(false);
expect(() => AccountEntity.create(email, "password123")).toThrow(
InvalidEmailFormatError,
);
});
});
it("should use same regex pattern", () => {
validEmails.forEach((email) => {
expect(EMAIL_REGEX.test(email)).toBe(true);
});
invalidEmails.forEach((email) => {
expect(EMAIL_REGEX.test(email)).toBe(false);
});
});
});
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 = { email: "test@example.com", password: "password123" };
const invalidLogin = { email: "invalid", password: "123" };
expect(loginSchema.safeParse(validLogin).success).toBe(true);
expect(loginSchema.safeParse(invalidLogin).success).toBe(false);
});
it("should validate register schema correctly", () => {
const validRegister = {
email: "test@example.com",
password: "password123",
};
const invalidRegister = { email: "invalid", 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 email error message", () => {
const result = emailSchema.safeParse("invalid-email");
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.message).toBe("Format d'email invalide");
}
});
});
});