core tests

This commit is contained in:
2026-01-24 02:05:43 +01:00
parent 78c9c1ec75
commit 3bef15f3bd
13 changed files with 379 additions and 267 deletions

View File

@@ -0,0 +1,790 @@
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
import { UserRole } from '@core/admin/domain/value-objects/UserRole';
import { UserStatus } from '@core/admin/domain/value-objects/UserStatus';
import { InMemoryAdminUserRepository } from './InMemoryAdminUserRepository';
describe('InMemoryAdminUserRepository', () => {
describe('TDD - Test First', () => {
let repository: InMemoryAdminUserRepository;
beforeEach(() => {
repository = new InMemoryAdminUserRepository();
});
describe('create', () => {
it('should create a new user', async () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
// Act
const result = await repository.create(user);
// Assert
expect(result).toStrictEqual(user);
const found = await repository.findById(user.id);
expect(found).toStrictEqual(user);
});
it('should throw error when creating user with duplicate email', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'test@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'test@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
await repository.create(user1);
// Act & Assert
await expect(repository.create(user2)).rejects.toThrow('Email already exists');
});
it('should throw error when creating user with duplicate ID', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-1',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
await repository.create(user1);
// Act & Assert
await expect(repository.create(user2)).rejects.toThrow('User ID already exists');
});
});
describe('findById', () => {
it('should find user by ID', async () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
await repository.create(user);
// Act
const found = await repository.findById(user.id);
// Assert
expect(found).toStrictEqual(user);
});
it('should return null for non-existent user', async () => {
// Arrange
const nonExistentId = AdminUser.create({
id: 'non-existent',
email: 'dummy@example.com',
displayName: 'Dummy',
roles: ['user'],
status: 'active',
}).id;
// Act
const found = await repository.findById(nonExistentId);
// Assert
expect(found).toBeNull();
});
});
describe('findByEmail', () => {
it('should find user by email', async () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
await repository.create(user);
// Act
const found = await repository.findByEmail(user.email);
// Assert
expect(found).toStrictEqual(user);
});
it('should return null for non-existent email', async () => {
// Arrange
const nonExistentEmail = AdminUser.create({
id: 'dummy',
email: 'non-existent@example.com',
displayName: 'Dummy',
roles: ['user'],
status: 'active',
}).email;
// Act
const found = await repository.findByEmail(nonExistentEmail);
// Assert
expect(found).toBeNull();
});
});
describe('emailExists', () => {
it('should return true when email exists', async () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
await repository.create(user);
// Act
const exists = await repository.emailExists(user.email);
// Assert
expect(exists).toBe(true);
});
it('should return false when email does not exist', async () => {
// Arrange
const nonExistentEmail = AdminUser.create({
id: 'dummy',
email: 'non-existent@example.com',
displayName: 'Dummy',
roles: ['user'],
status: 'active',
}).email;
// Act
const exists = await repository.emailExists(nonExistentEmail);
// Assert
expect(exists).toBe(false);
});
});
describe('list', () => {
it('should return empty list when no users exist', async () => {
// Act
const result = await repository.list({});
// Assert
expect(result.users).toEqual([]);
expect(result.total).toBe(0);
expect(result.page).toBe(1);
expect(result.limit).toBe(10);
expect(result.totalPages).toBe(0);
});
it('should return all users when no filters provided', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['admin'],
status: 'active',
});
await repository.create(user1);
await repository.create(user2);
// Act
const result = await repository.list({});
// Assert
expect(result.users).toHaveLength(2);
expect(result.total).toBe(2);
expect(result.page).toBe(1);
expect(result.limit).toBe(10);
expect(result.totalPages).toBe(1);
});
it('should filter by role', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
});
const admin1 = AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin 1',
roles: ['admin'],
status: 'active',
});
await repository.create(user1);
await repository.create(admin1);
// Act
const result = await repository.list({
filter: { role: UserRole.fromString('admin') },
});
// Assert
expect(result.users).toHaveLength(1);
expect(result.users[0]?.id.value).toBe('admin-1');
expect(result.total).toBe(1);
});
it('should filter by status', async () => {
// Arrange
const activeUser = AdminUser.create({
id: 'user-1',
email: 'active@example.com',
displayName: 'Active User',
roles: ['user'],
status: 'active',
});
const suspendedUser = AdminUser.create({
id: 'user-2',
email: 'suspended@example.com',
displayName: 'Suspended User',
roles: ['user'],
status: 'suspended',
});
await repository.create(activeUser);
await repository.create(suspendedUser);
// Act
const result = await repository.list({
filter: { status: UserStatus.fromString('suspended') },
});
// Assert
expect(result.users).toHaveLength(1);
expect(result.users[0]?.id.value).toBe('user-2');
expect(result.total).toBe(1);
});
it('should filter by email', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'other@example.com',
displayName: 'Other User',
roles: ['user'],
status: 'active',
});
await repository.create(user1);
await repository.create(user2);
// Act
const result = await repository.list({
filter: { email: user1.email },
});
// Assert
expect(result.users).toHaveLength(1);
expect(result.users[0]?.id.value).toBe('user-1');
expect(result.total).toBe(1);
});
it('should filter by search (email or display name)', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'search@example.com',
displayName: 'Search User',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'other@example.com',
displayName: 'Other User',
roles: ['user'],
status: 'active',
});
await repository.create(user1);
await repository.create(user2);
// Act
const result = await repository.list({
filter: { search: 'search' },
});
// Assert
expect(result.users).toHaveLength(1);
expect(result.users[0]?.id.value).toBe('user-1');
expect(result.total).toBe(1);
});
it('should apply pagination', async () => {
// Arrange - Create 15 users
for (let i = 1; i <= 15; i++) {
const user = AdminUser.create({
id: `user-${i}`,
email: `user${i}@example.com`,
displayName: `User ${i}`,
roles: ['user'],
status: 'active',
});
await repository.create(user);
}
// Act - Get page 2 with limit 5
const result = await repository.list({
pagination: { page: 2, limit: 5 },
});
// Assert
expect(result.users).toHaveLength(5);
expect(result.total).toBe(15);
expect(result.page).toBe(2);
expect(result.limit).toBe(5);
expect(result.totalPages).toBe(3);
expect(result.users[0]?.id.value).toBe('user-6');
});
it('should sort by email ascending', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'zebra@example.com',
displayName: 'Zebra',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'alpha@example.com',
displayName: 'Alpha',
roles: ['user'],
status: 'active',
});
await repository.create(user1);
await repository.create(user2);
// Act
const result = await repository.list({
sort: { field: 'email', direction: 'asc' },
});
// Assert
expect(result.users).toHaveLength(2);
expect(result.users[0]?.id.value).toBe('user-2');
expect(result.users[1]?.id.value).toBe('user-1');
});
it('should sort by display name descending', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'Zebra',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'Alpha',
roles: ['user'],
status: 'active',
});
await repository.create(user1);
await repository.create(user2);
// Act
const result = await repository.list({
sort: { field: 'displayName', direction: 'desc' },
});
// Assert
expect(result.users).toHaveLength(2);
expect(result.users[0]?.id.value).toBe('user-1');
expect(result.users[1]?.id.value).toBe('user-2');
});
it('should apply multiple filters together', async () => {
// Arrange
const matchingUser = AdminUser.create({
id: 'user-1',
email: 'admin@example.com',
displayName: 'Admin User',
roles: ['admin'],
status: 'active',
});
const nonMatchingUser1 = AdminUser.create({
id: 'user-2',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const nonMatchingUser2 = AdminUser.create({
id: 'user-3',
email: 'admin-suspended@example.com',
displayName: 'Admin User',
roles: ['admin'],
status: 'suspended',
});
await repository.create(matchingUser);
await repository.create(nonMatchingUser1);
await repository.create(nonMatchingUser2);
// Act
const result = await repository.list({
filter: {
role: UserRole.fromString('admin'),
status: UserStatus.fromString('active'),
search: 'admin',
},
});
// Assert
expect(result.users).toHaveLength(1);
expect(result.users[0]?.id.value).toBe('user-1');
expect(result.total).toBe(1);
});
});
describe('count', () => {
it('should return 0 when no users exist', async () => {
// Act
const count = await repository.count();
// Assert
expect(count).toBe(0);
});
it('should return correct count of all users', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['admin'],
status: 'active',
});
await repository.create(user1);
await repository.create(user2);
// Act
const count = await repository.count();
// Assert
expect(count).toBe(2);
});
it('should count with filters', async () => {
// Arrange
const activeUser = AdminUser.create({
id: 'user-1',
email: 'active@example.com',
displayName: 'Active User',
roles: ['user'],
status: 'active',
});
const suspendedUser = AdminUser.create({
id: 'user-2',
email: 'suspended@example.com',
displayName: 'Suspended User',
roles: ['user'],
status: 'suspended',
});
await repository.create(activeUser);
await repository.create(suspendedUser);
// Act
const count = await repository.count({
status: UserStatus.fromString('active'),
});
// Assert
expect(count).toBe(1);
});
});
describe('update', () => {
it('should update existing user', async () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
await repository.create(user);
// Act
user.updateDisplayName('Updated Name');
const updated = await repository.update(user);
// Assert
expect(updated.displayName).toBe('Updated Name');
const found = await repository.findById(user.id);
expect(found?.displayName).toBe('Updated Name');
});
it('should throw error when updating non-existent user', async () => {
// Arrange
const user = AdminUser.create({
id: 'non-existent',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
// Act & Assert
await expect(repository.update(user)).rejects.toThrow('User not found');
});
it('should throw error when updating with duplicate email', async () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
await repository.create(user1);
await repository.create(user2);
// Act - Try to change user2's email to user1's email
user2.updateEmail(user1.email);
// Assert
await expect(repository.update(user2)).rejects.toThrow('Email already exists');
});
});
describe('delete', () => {
it('should delete existing user', async () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
await repository.create(user);
// Act
await repository.delete(user.id);
// Assert
const found = await repository.findById(user.id);
expect(found).toBeNull();
});
it('should throw error when deleting non-existent user', async () => {
// Arrange
const nonExistentId = AdminUser.create({
id: 'non-existent',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
}).id;
// Act & Assert
await expect(repository.delete(nonExistentId)).rejects.toThrow('User not found');
});
it('should reduce count after deletion', async () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
await repository.create(user);
const initialCount = await repository.count();
// Act
await repository.delete(user.id);
// Assert
const finalCount = await repository.count();
expect(finalCount).toBe(initialCount - 1);
});
});
describe('integration scenarios', () => {
it('should handle complete user lifecycle', async () => {
// Arrange - Create user
const user = AdminUser.create({
id: 'user-1',
email: 'lifecycle@example.com',
displayName: 'Lifecycle User',
roles: ['user'],
status: 'active',
});
// Act - Create
await repository.create(user);
let found = await repository.findById(user.id);
expect(found).toStrictEqual(user);
// Act - Update
user.updateDisplayName('Updated Lifecycle');
user.addRole(UserRole.fromString('admin'));
await repository.update(user);
found = await repository.findById(user.id);
expect(found?.displayName).toBe('Updated Lifecycle');
expect(found?.hasRole('admin')).toBe(true);
// Act - Delete
await repository.delete(user.id);
found = await repository.findById(user.id);
expect(found).toBeNull();
});
it('should handle complex filtering and pagination', async () => {
// Arrange - Create mixed users
const users = [
AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner User',
roles: ['owner'],
status: 'active',
}),
AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin One',
roles: ['admin'],
status: 'active',
}),
AdminUser.create({
id: 'admin-2',
email: 'admin2@example.com',
displayName: 'Admin Two',
roles: ['admin'],
status: 'suspended',
}),
AdminUser.create({
id: 'user-1',
email: 'user1@example.com',
displayName: 'User One',
roles: ['user'],
status: 'active',
}),
AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User Two',
roles: ['user'],
status: 'active',
}),
];
for (const user of users) {
await repository.create(user);
}
// Act - Get active admins, sorted by email, page 1, limit 2
const result = await repository.list({
filter: {
role: UserRole.fromString('admin'),
status: UserStatus.fromString('active'),
},
sort: { field: 'email', direction: 'asc' },
pagination: { page: 1, limit: 2 },
});
// Assert
expect(result.users).toHaveLength(1);
expect(result.users[0]?.id.value).toBe('admin-1');
expect(result.total).toBe(1);
expect(result.totalPages).toBe(1);
});
});
});
});

View File

@@ -0,0 +1,257 @@
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
import { AdminUserRepository, StoredAdminUser, UserFilter, UserListQuery, UserListResult } from '@core/admin/domain/repositories/AdminUserRepository';
import { Email } from '@core/admin/domain/value-objects/Email';
import { UserId } from '@core/admin/domain/value-objects/UserId';
/**
* In-memory implementation of AdminUserRepository for testing and development
* Follows TDD - created with tests first
*/
export class InMemoryAdminUserRepository implements AdminUserRepository {
private storage: Map<string, StoredAdminUser> = new Map();
async findById(id: UserId): Promise<AdminUser | null> {
const stored = this.storage.get(id.value);
return stored ? this.fromStored(stored) : null;
}
async findByEmail(email: Email): Promise<AdminUser | null> {
for (const stored of this.storage.values()) {
if (stored.email === email.value) {
return this.fromStored(stored);
}
}
return null;
}
async emailExists(email: Email): Promise<boolean> {
for (const stored of this.storage.values()) {
if (stored.email === email.value) {
return true;
}
}
return false;
}
async existsById(id: UserId): Promise<boolean> {
return this.storage.has(id.value);
}
async existsByEmail(email: Email): Promise<boolean> {
return this.emailExists(email);
}
async list(query?: UserListQuery): Promise<UserListResult> {
let users: AdminUser[] = [];
// Get all users
for (const stored of this.storage.values()) {
users.push(this.fromStored(stored));
}
// Apply filters
if (query?.filter) {
users = users.filter(user => {
if (query.filter?.role && !user.roles.some(r => r.equals(query.filter!.role!))) {
return false;
}
if (query.filter?.status && !user.status.equals(query.filter.status)) {
return false;
}
if (query.filter?.email && !user.email.equals(query.filter.email)) {
return false;
}
if (query.filter?.search) {
const search = query.filter.search.toLowerCase();
const matchesEmail = user.email.value.toLowerCase().includes(search);
const matchesDisplayName = user.displayName.toLowerCase().includes(search);
if (!matchesEmail && !matchesDisplayName) {
return false;
}
}
return true;
});
}
const total = users.length;
// Apply sorting
if (query?.sort) {
const { field, direction } = query.sort;
users.sort((a, b) => {
let aVal: string | number | Date;
let bVal: string | number | Date;
switch (field) {
case 'email':
aVal = a.email.value;
bVal = b.email.value;
break;
case 'displayName':
aVal = a.displayName;
bVal = b.displayName;
break;
case 'createdAt':
aVal = a.createdAt;
bVal = b.createdAt;
break;
case 'lastLoginAt':
aVal = a.lastLoginAt || 0;
bVal = b.lastLoginAt || 0;
break;
case 'status':
aVal = a.status.value;
bVal = b.status.value;
break;
default:
return 0;
}
if (aVal < bVal) return direction === 'asc' ? -1 : 1;
if (aVal > bVal) return direction === 'asc' ? 1 : -1;
return 0;
});
}
// Apply pagination
const page = query?.pagination?.page || 1;
const limit = query?.pagination?.limit || 10;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedUsers = users.slice(start, end);
return {
users: paginatedUsers,
total,
page,
limit,
totalPages,
};
}
async count(filter?: UserFilter): Promise<number> {
let count = 0;
for (const stored of this.storage.values()) {
const user = this.fromStored(stored);
if (filter?.role && !user.roles.some(r => r.equals(filter.role!))) {
continue;
}
if (filter?.status && !user.status.equals(filter.status)) {
continue;
}
if (filter?.email && !user.email.equals(filter.email)) {
continue;
}
if (filter?.search) {
const search = filter.search.toLowerCase();
const matchesEmail = user.email.value.toLowerCase().includes(search);
const matchesDisplayName = user.displayName.toLowerCase().includes(search);
if (!matchesEmail && !matchesDisplayName) {
continue;
}
}
count++;
}
return count;
}
async create(user: AdminUser): Promise<AdminUser> {
// Check for duplicate email
if (await this.emailExists(user.email)) {
throw new Error('Email already exists');
}
// Check for duplicate ID
if (this.storage.has(user.id.value)) {
throw new Error('User ID already exists');
}
const stored = this.toStored(user);
this.storage.set(user.id.value, stored);
return user;
}
async update(user: AdminUser): Promise<AdminUser> {
// Check if user exists
if (!this.storage.has(user.id.value)) {
throw new Error('User not found');
}
// Check for duplicate email (excluding current user)
for (const [id, stored] of this.storage.entries()) {
if (id !== user.id.value && stored.email === user.email.value) {
throw new Error('Email already exists');
}
}
const stored = this.toStored(user);
this.storage.set(user.id.value, stored);
return user;
}
async delete(id: UserId): Promise<void> {
if (!this.storage.has(id.value)) {
throw new Error('User not found');
}
this.storage.delete(id.value);
}
toStored(user: AdminUser): StoredAdminUser {
const stored: StoredAdminUser = {
id: user.id.value,
email: user.email.value,
roles: user.roles.map(r => r.value),
status: user.status.value,
displayName: user.displayName,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
if (user.lastLoginAt !== undefined) {
stored.lastLoginAt = user.lastLoginAt;
}
if (user.primaryDriverId !== undefined) {
stored.primaryDriverId = user.primaryDriverId;
}
return stored;
}
fromStored(stored: StoredAdminUser): AdminUser {
const props: {
id: string;
email: string;
roles: string[];
status: string;
displayName: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
} = {
id: stored.id,
email: stored.email,
roles: stored.roles,
status: stored.status,
displayName: stored.displayName,
createdAt: stored.createdAt,
updatedAt: stored.updatedAt,
};
if (stored.lastLoginAt !== undefined) {
props.lastLoginAt = stored.lastLoginAt;
}
if (stored.primaryDriverId !== undefined) {
props.primaryDriverId = stored.primaryDriverId;
}
return AdminUser.rehydrate(props);
}
}