Files
trackgo-be/src/modules/configuration/branches/branches.repository.spec.ts
T
shancheas f635ebeda0 Enhance branch management with foreign key relation handling
- Updated `BranchesModule` to include foreign key relations in list and write responses, ensuring they are represented as nested objects using `pickRelation`.
- Introduced new `relation-response.mdc` file to define guidelines for embedding foreign key relations.
- Modified `BranchesRepository` to support fetching related `division`, `createdByUser`, and `updatedByUser` data.
- Updated DTOs and service methods to reflect changes in response structure, removing direct foreign key IDs.
- Added unit tests to validate the new relation handling in branches service and repository.
- Enhanced e2e tests to verify the correct structure of branch responses with nested relations.
2026-08-26 13:44:13 +07:00

295 lines
8.8 KiB
TypeScript

import {
BadRequestException,
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 { BranchesRepository } from './branches.repository';
describe('BranchesRepository', () => {
let repository: BranchesRepository;
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 leftJoin = jest.fn();
const db = {
select,
insert,
update,
delete: del,
transaction,
};
const row = {
id: 'br-1',
code: 'JKT_01',
name: 'Jakarta Pusat',
phone: '+6281234567890',
address: 'Jl Sudirman No 1',
latitude: -6.2,
longitude: 106.8,
workingDaysStart: 'monday',
workingDaysEnd: 'friday',
workingHoursStart: '08:00',
workingHoursEnd: '17:00',
nfcId: 'NFC-001',
divisionId: 'div-1',
status: 'draft',
createdAt: 1_700_000_000_000,
updatedAt: 1_700_000_000_000,
createdBy: 'user-1',
updatedBy: 'user-1',
};
const joinedRow = {
branch: row,
division: { id: 'div-1', code: 'JKT', name: 'Jakarta' },
createdByUser: { id: 'user-1', username: 'admin' },
updatedByUser: { id: 'user-1', username: 'admin' },
};
const createInput = {
code: 'JKT_01',
name: 'Jakarta Pusat',
phone: PhoneNumber.create('+6281234567890'),
address: 'Jl Sudirman No 1',
workingDaysStart: 'monday',
workingDaysEnd: 'friday',
workingHoursStart: '08:00',
workingHoursEnd: '17:00',
userId: 'user-1',
};
const joinChain = () => {
const chain: {
leftJoin: jest.Mock;
where: typeof where;
$dynamic: typeof $dynamic;
} = {
leftJoin: jest.fn(),
where,
$dynamic,
};
chain.leftJoin.mockReturnValue(chain);
return chain;
};
beforeEach(async () => {
jest.clearAllMocks();
where.mockImplementation(() => ({ limit, orderBy, returning }));
orderBy.mockImplementation(() => ({ limit }));
limit.mockImplementation(() => ({
offset,
then: (
resolve: (value: (typeof joinedRow)[]) => unknown,
reject?: (reason: unknown) => unknown,
) => Promise.resolve([joinedRow]).then(resolve, reject),
}));
offset.mockResolvedValue([joinedRow]);
from.mockImplementation(() => joinChain());
leftJoin.mockImplementation(() => joinChain());
$dynamic.mockReturnValue({ where });
select.mockImplementation(() => ({ from }));
values.mockReturnValue({ returning });
insert.mockReturnValue({ values });
set.mockReturnValue({ where });
update.mockReturnValue({ set });
del.mockReturnValue({ where });
returning.mockResolvedValue([row]);
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [BranchesRepository, { provide: DRIZZLE, useValue: db }],
}).compile();
repository = moduleRef.get(BranchesRepository);
});
it('findById maps a row to domain Branch', async () => {
const branch = await repository.findById('br-1');
expect(branch).toMatchObject({
id: 'br-1',
code: 'JKT_01',
name: 'Jakarta Pusat',
createdBy: 'user-1',
division: { id: 'div-1', code: 'JKT', name: 'Jakarta' },
createdByUser: { id: 'user-1', username: 'admin' },
updatedByUser: { id: 'user-1', username: 'admin' },
});
expect(branch?.phone.value).toBe('+6281234567890');
expect(branch?.status.value).toBe('draft');
expect(branch?.createdAt.value).toBe(1_700_000_000_000);
});
it('findById returns null when missing', async () => {
limit.mockImplementationOnce(() => Promise.resolve([]));
await expect(repository.findById('missing')).resolves.toBeNull();
});
it('findByCode maps a row', async () => {
const branch = await repository.findByCode('JKT_01');
expect(branch?.code).toBe('JKT_01');
});
it('list returns mapped rows and total', async () => {
select
.mockImplementationOnce(() => ({
from: () => ({
where: () => Promise.resolve([{ total: 1 }]),
}),
}))
.mockImplementationOnce(() => ({
from: () => {
const chain = joinChain();
return chain;
},
}));
const result = await repository.list({
name: 'Jakarta',
code: 'JKT',
phone: '+628',
address: 'Sudirman',
divisionId: 'div-1',
nfcId: 'NFC-001',
status: 'draft',
workingDaysStart: 'monday',
workingDaysEnd: 'friday',
workingHoursStart: '08:00',
workingHoursEnd: '17:00',
search: 'sudirman',
limit: 10,
offset: 0,
});
expect(result.total).toBe(1);
expect(result.data[0].code).toBe('JKT_01');
expect(result.data[0].division).toEqual({
id: 'div-1',
code: 'JKT',
name: 'Jakarta',
});
});
it('create inserts and maps unique violations', async () => {
returning.mockResolvedValueOnce([row]);
const created = await repository.create(createInput);
expect(created.code).toBe('JKT_01');
expect(created.createdByUser).toEqual({ id: 'user-1', username: 'admin' });
returning.mockRejectedValueOnce({ code: '23505' });
await expect(repository.create(createInput)).rejects.toBeInstanceOf(
ConflictException,
);
returning.mockRejectedValueOnce({
code: '23505',
constraint: 'branches_nfc_id_unique',
});
await expect(repository.create(createInput)).rejects.toMatchObject({
message: 'Branch NFC ID already exists',
});
});
it('create maps missing division foreign keys', async () => {
returning.mockRejectedValueOnce({ code: '23503' });
await expect(repository.create(createInput)).rejects.toBeInstanceOf(
BadRequestException,
);
});
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('updateStatus returns the mapped row', async () => {
returning.mockResolvedValueOnce([row]);
const updated = await repository.updateStatus(
'br-1',
Status.create('active'),
'user-1',
);
expect(updated.id).toBe('br-1');
expect(updated.updatedByUser).toEqual({ id: 'user-1', username: 'admin' });
});
it('update throws when missing', async () => {
limit.mockImplementationOnce(() => Promise.resolve([]));
await expect(
repository.update('missing', { userId: 'user-1' }),
).rejects.toBeInstanceOf(NotFoundException);
});
it('update maps a row when present', async () => {
returning.mockResolvedValueOnce([row]);
const updated = await repository.update('br-1', {
name: 'Jakarta Selatan',
userId: 'user-1',
});
expect(updated.code).toBe('JKT_01');
});
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('bulkUpdateStatus and bulkDelete return affected counts', async () => {
returning.mockResolvedValue([{ id: 'br-1' }, { id: 'br-2' }]);
await expect(
repository.bulkUpdateStatus(
['br-1', 'br-2'],
Status.create('active'),
'user-1',
),
).resolves.toBe(2);
returning.mockResolvedValue([{ id: 'br-1' }]);
await expect(repository.bulkDelete(['br-1'])).resolves.toBe(1);
});
it('extendListQuery is a passthrough hook', () => {
const qb = { join: true };
expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb);
});
});