Files
trackgo-be/src/modules/configuration/customers/customers.repository.spec.ts
T
shancheas 4c45a4371e Enhance pagination and ordering capabilities in API responses
- Updated pagination-response and read-write-controllers documentation to include `orderBy` and `orderType` parameters for sorting results.
- Introduced new `order-clause` module to handle ordering logic, including validation for order types and columns.
- Enhanced `PaginationQueryDto` to support ordering fields in API requests.
- Updated various repository and service classes to implement ordering in database queries.
- Added unit tests for new ordering functionality and ensured existing tests cover the updated behavior.
- Refactored related DTOs to include user and code relations for better data representation in responses.
2026-08-27 13:09:41 +07:00

239 lines
7.0 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 { CustomersRepository } from './customers.repository';
describe('CustomersRepository', () => {
let repository: CustomersRepository;
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: 'cu-1',
code: 'CUST_01',
name: 'Acme Corp',
phone: '+6281234567890',
address: 'Jl Sudirman No 1',
latitude: -6.2,
longitude: 106.8,
nfcId: 'NFC-001',
status: 'draft',
createdAt: 1_700_000_000_000,
updatedAt: 1_700_000_000_000,
createdBy: 'user-1',
updatedBy: 'user-1',
};
const contactRow = {
id: 'ct-1',
customerId: 'cu-1',
name: 'Jean Luc',
jobTitle: 'Buyer',
phone: '+6281234567891',
mobilePhone: null,
notes: 'Primary',
};
const createInput = {
code: 'CUST_01',
name: 'Acme Corp',
phone: PhoneNumber.create('+6281234567890'),
address: 'Jl Sudirman No 1',
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: [CustomersRepository, { provide: DRIZZLE, useValue: db }],
}).compile();
repository = moduleRef.get(CustomersRepository);
});
it('findById maps a row and contacts to domain', async () => {
limit.mockResolvedValueOnce([row]);
orderBy.mockResolvedValueOnce([contactRow]);
const customer = await repository.findById('cu-1');
expect(customer).toMatchObject({
id: 'cu-1',
code: 'CUST_01',
createdBy: 'user-1',
});
expect(customer?.phone.value).toBe('+6281234567890');
expect(customer?.status.value).toBe('draft');
expect(customer?.contacts[0].name).toBe('Jean Luc');
expect(customer?.contacts[0].phone?.value).toBe('+6281234567891');
});
it('findById returns null when missing', async () => {
limit.mockResolvedValueOnce([]);
await expect(repository.findById('missing')).resolves.toBeNull();
});
it('list returns mapped rows and total without loading contacts', 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: 'Acme',
code: 'CUST',
search: 'sudirman',
limit: 10,
offset: 0,
});
expect(result.total).toBe(1);
expect(result.data[0].code).toBe('CUST_01');
expect(result.data[0].contacts).toEqual([]);
});
it('create inserts and maps unique violations', async () => {
returning.mockResolvedValue([row]);
orderBy.mockResolvedValueOnce([]);
const created = await repository.create(createInput);
expect(created.code).toBe('CUST_01');
transaction.mockRejectedValueOnce({ code: '23505' });
await expect(repository.create(createInput)).rejects.toBeInstanceOf(
ConflictException,
);
transaction.mockRejectedValueOnce({
code: '23505',
constraint: 'customers_nfc_id_unique',
});
await expect(repository.create(createInput)).rejects.toMatchObject({
message: 'Customer NFC ID already exists',
});
transaction.mockRejectedValueOnce({
cause: {
code: '23505',
constraint_name: 'customers_code_unique',
},
});
await expect(repository.create(createInput)).rejects.toMatchObject({
message: 'Customer code already exists',
});
});
it('create rethrows unknown errors', async () => {
transaction.mockRejectedValue(new Error('db down'));
await expect(repository.create(createInput)).rejects.toThrow('db down');
});
it('createMany returns 0 for an empty batch', async () => {
await expect(repository.createMany([])).resolves.toBe(0);
});
it('update throws when missing', async () => {
limit.mockResolvedValueOnce([]);
await expect(
repository.update('missing', { userId: 'user-1' }),
).rejects.toBeInstanceOf(NotFoundException);
});
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('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('addContact throws when customer is missing', async () => {
limit.mockResolvedValueOnce([]);
await expect(
repository.addContact('missing', { name: 'Ada' }, 'user-1'),
).rejects.toBeInstanceOf(NotFoundException);
});
it('deleteContact throws when contact is missing', async () => {
returning.mockResolvedValueOnce([]);
await expect(
repository.deleteContact('cu-1', 'ct-1'),
).rejects.toBeInstanceOf(NotFoundException);
});
it('extendListQuery is a passthrough hook', () => {
const qb = { join: true };
expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb);
});
});