- Enhanced the `delete` and `bulkDelete` methods in `EmployeesRepository` to handle foreign key violations by throwing a `ConflictException` with a descriptive message. - Added unit tests to verify that foreign key violations are correctly mapped to `ConflictException` and that unknown errors are rethrown as expected. - Refactored error handling in the `delete` methods to improve clarity and maintainability.
278 lines
8.5 KiB
TypeScript
278 lines
8.5 KiB
TypeScript
import { ConflictException, NotFoundException } from '@nestjs/common';
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { PhoneNumber } from '../../../common/value-objects/phone-number/phone-number';
|
|
import { Status } from '../../../common/value-objects/status/status';
|
|
import { DRIZZLE } from '../../../database/database.module';
|
|
import { EmployeesRepository } from './employees.repository';
|
|
|
|
describe('EmployeesRepository', () => {
|
|
let repository: EmployeesRepository;
|
|
|
|
const limit = jest.fn();
|
|
const orderBy = jest.fn();
|
|
const offset = jest.fn();
|
|
const where = jest.fn();
|
|
const from = jest.fn();
|
|
const select = jest.fn();
|
|
const returning = jest.fn();
|
|
const values = jest.fn();
|
|
const insert = jest.fn();
|
|
const set = jest.fn();
|
|
const update = jest.fn();
|
|
const del = jest.fn();
|
|
const transaction = jest.fn();
|
|
const $dynamic = jest.fn();
|
|
|
|
const db = {
|
|
select,
|
|
insert,
|
|
update,
|
|
delete: del,
|
|
transaction,
|
|
};
|
|
|
|
const row = {
|
|
id: 'emp-1',
|
|
code: 'EMP_01',
|
|
name: 'Ada Lovelace',
|
|
phone: '+6281234567890',
|
|
position: 'sales',
|
|
status: 'draft',
|
|
createdAt: 1_700_000_000_000,
|
|
updatedAt: 1_700_000_000_000,
|
|
createdBy: 'user-1',
|
|
updatedBy: 'user-1',
|
|
userId: null,
|
|
};
|
|
|
|
const createInput = {
|
|
code: 'EMP_01',
|
|
name: 'Ada Lovelace',
|
|
phone: PhoneNumber.create('+6281234567890'),
|
|
position: 'sales' as const,
|
|
userId: 'user-1',
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
jest.clearAllMocks();
|
|
where.mockImplementation(() =>
|
|
Object.assign(Promise.resolve([{ id: 'user-1', username: 'admin' }]), {
|
|
limit,
|
|
orderBy,
|
|
returning,
|
|
}),
|
|
);
|
|
orderBy.mockImplementation(() => ({ limit }));
|
|
limit.mockImplementation(() => ({ offset }));
|
|
offset.mockResolvedValue([row]);
|
|
from.mockImplementation(() => ({
|
|
where,
|
|
$dynamic,
|
|
}));
|
|
$dynamic.mockReturnValue({ where });
|
|
select.mockImplementation(() => ({ from }));
|
|
values.mockReturnValue({ returning });
|
|
insert.mockReturnValue({ values });
|
|
set.mockReturnValue({ where });
|
|
update.mockReturnValue({ set });
|
|
del.mockReturnValue({ where });
|
|
returning.mockResolvedValue([row]);
|
|
transaction.mockImplementation((fn: (tx: typeof db) => unknown) => fn(db));
|
|
|
|
const moduleRef: TestingModule = await Test.createTestingModule({
|
|
providers: [EmployeesRepository, { provide: DRIZZLE, useValue: db }],
|
|
}).compile();
|
|
repository = moduleRef.get(EmployeesRepository);
|
|
});
|
|
|
|
it('findById maps a row to domain Employee', async () => {
|
|
limit.mockResolvedValueOnce([row]);
|
|
const employee = await repository.findById('emp-1');
|
|
expect(employee).toMatchObject({
|
|
id: 'emp-1',
|
|
code: 'EMP_01',
|
|
name: 'Ada Lovelace',
|
|
position: 'sales',
|
|
createdBy: 'user-1',
|
|
});
|
|
expect(employee?.phone.value).toBe('+6281234567890');
|
|
expect(employee?.status.value).toBe('draft');
|
|
expect(employee?.createdAt.value).toBe(1_700_000_000_000);
|
|
});
|
|
|
|
it('findById returns null when missing', async () => {
|
|
limit.mockResolvedValueOnce([]);
|
|
await expect(repository.findById('missing')).resolves.toBeNull();
|
|
});
|
|
|
|
it('findByCode maps a row', async () => {
|
|
limit.mockResolvedValueOnce([row]);
|
|
const employee = await repository.findByCode('EMP_01');
|
|
expect(employee?.code).toBe('EMP_01');
|
|
});
|
|
|
|
it('findByUserId maps a row', async () => {
|
|
limit.mockResolvedValueOnce([row]);
|
|
const employee = await repository.findByUserId('user-1');
|
|
expect(employee?.id).toBe('emp-1');
|
|
});
|
|
|
|
it('list returns mapped rows and total', async () => {
|
|
select
|
|
.mockImplementationOnce(() => ({
|
|
from: () => ({
|
|
where: () => Promise.resolve([{ total: 1 }]),
|
|
}),
|
|
}))
|
|
.mockImplementationOnce(() => ({
|
|
from: () => ({
|
|
$dynamic: () => ({
|
|
where: () => ({
|
|
orderBy: () => ({
|
|
limit: () => ({
|
|
offset: () => Promise.resolve([row]),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
const result = await repository.list({
|
|
name: 'Ada',
|
|
code: 'EMP',
|
|
phone: '+628',
|
|
position: ['sales', 'driver'],
|
|
status: 'draft',
|
|
search: 'ada',
|
|
limit: 10,
|
|
offset: 0,
|
|
});
|
|
expect(result.total).toBe(1);
|
|
expect(result.data[0].code).toBe('EMP_01');
|
|
expect(result.data[0].phone.value).toBe('+6281234567890');
|
|
});
|
|
|
|
it('create inserts and maps unique violations', async () => {
|
|
returning.mockResolvedValueOnce([row]);
|
|
const created = await repository.create(createInput);
|
|
expect(created.code).toBe('EMP_01');
|
|
|
|
returning.mockRejectedValueOnce({ code: '23505' });
|
|
await expect(repository.create(createInput)).rejects.toBeInstanceOf(
|
|
ConflictException,
|
|
);
|
|
|
|
returning.mockRejectedValueOnce({
|
|
cause: { code: '23505' },
|
|
});
|
|
await expect(repository.create(createInput)).rejects.toBeInstanceOf(
|
|
ConflictException,
|
|
);
|
|
});
|
|
|
|
it('create rethrows unknown errors', async () => {
|
|
returning.mockRejectedValue(new Error('db down'));
|
|
await expect(repository.create(createInput)).rejects.toThrow('db down');
|
|
});
|
|
|
|
it('createMany returns 0 for an empty batch and inserts otherwise', async () => {
|
|
await expect(repository.createMany([])).resolves.toBe(0);
|
|
transaction.mockImplementation(
|
|
async (fn: (tx: typeof db) => Promise<void>) => {
|
|
await fn(db);
|
|
},
|
|
);
|
|
await expect(repository.createMany([createInput])).resolves.toBe(1);
|
|
});
|
|
|
|
it('createMany maps unique violations', async () => {
|
|
transaction.mockRejectedValue({ code: '23505' });
|
|
await expect(repository.createMany([createInput])).rejects.toBeInstanceOf(
|
|
ConflictException,
|
|
);
|
|
});
|
|
|
|
it('update throws when missing and maps unique violations', async () => {
|
|
limit.mockResolvedValueOnce([]);
|
|
await expect(
|
|
repository.update('missing', { userId: 'user-1' }),
|
|
).rejects.toBeInstanceOf(NotFoundException);
|
|
|
|
limit.mockResolvedValueOnce([row]);
|
|
returning.mockRejectedValueOnce({ code: '23505' });
|
|
await expect(
|
|
repository.update('emp-1', { code: 'EMP_02', userId: 'user-1' }),
|
|
).rejects.toBeInstanceOf(ConflictException);
|
|
});
|
|
|
|
it('updateStatus throws when missing', async () => {
|
|
returning.mockResolvedValueOnce([]);
|
|
await expect(
|
|
repository.updateStatus('missing', Status.create('active'), 'user-1'),
|
|
).rejects.toBeInstanceOf(NotFoundException);
|
|
});
|
|
|
|
it('delete throws when missing', async () => {
|
|
returning.mockResolvedValueOnce([]);
|
|
await expect(repository.delete('missing')).rejects.toBeInstanceOf(
|
|
NotFoundException,
|
|
);
|
|
});
|
|
|
|
it('delete maps foreign-key violations to ConflictException', async () => {
|
|
returning.mockRejectedValueOnce({ code: '23503' });
|
|
await expect(repository.delete('emp-1')).rejects.toMatchObject({
|
|
constructor: ConflictException,
|
|
message: 'Employee is referenced by other records',
|
|
});
|
|
|
|
returning.mockRejectedValueOnce({
|
|
cause: { code: '23503' },
|
|
});
|
|
await expect(repository.delete('emp-1')).rejects.toBeInstanceOf(
|
|
ConflictException,
|
|
);
|
|
});
|
|
|
|
it('delete rethrows unknown errors', async () => {
|
|
returning.mockRejectedValueOnce(new Error('db down'));
|
|
await expect(repository.delete('emp-1')).rejects.toThrow('db down');
|
|
});
|
|
|
|
it('bulkUpdateStatus and bulkDelete return 0 for empty ids', async () => {
|
|
await expect(
|
|
repository.bulkUpdateStatus([], Status.create('active'), 'user-1'),
|
|
).resolves.toBe(0);
|
|
await expect(repository.bulkDelete([])).resolves.toBe(0);
|
|
});
|
|
|
|
it('bulkUpdateStatus and bulkDelete return affected counts', async () => {
|
|
returning.mockResolvedValue([{ id: 'emp-1' }, { id: 'emp-2' }]);
|
|
await expect(
|
|
repository.bulkUpdateStatus(
|
|
['emp-1', 'emp-2'],
|
|
Status.create('active'),
|
|
'user-1',
|
|
),
|
|
).resolves.toBe(2);
|
|
returning.mockResolvedValue([{ id: 'emp-1' }]);
|
|
await expect(repository.bulkDelete(['emp-1'])).resolves.toBe(1);
|
|
});
|
|
|
|
it('bulkDelete maps foreign-key violations to ConflictException', async () => {
|
|
returning.mockRejectedValueOnce({
|
|
cause: { code: '23503' },
|
|
});
|
|
await expect(repository.bulkDelete(['emp-1'])).rejects.toMatchObject({
|
|
constructor: ConflictException,
|
|
message: 'Employee is referenced by other records',
|
|
});
|
|
});
|
|
|
|
it('extendListQuery is a passthrough hook', () => {
|
|
const qb = { join: true };
|
|
expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb);
|
|
});
|
|
});
|