Files
trackgo-be/src/modules/field/cycles/cycles.service.spec.ts
T
shancheas 51db4f4a4d Refactor privilege management to support hierarchical privilege keys
- Updated privilege key structure to use a 3- or 4-part dotted hierarchy (e.g., `GROUP.PARENT.MODULE`).
- Modified the `RequirePrivilege` decorator to accept multiple keys, allowing for OR logic in privilege checks.
- Enhanced `PrivilegesGuard` to validate against multiple privilege keys, improving access control logic.
- Created migration scripts to update existing privilege keys in the database to the new format.
- Updated related services, controllers, and tests to accommodate the new privilege key structure and validation logic.
2026-09-01 13:14:58 +07:00

193 lines
5.8 KiB
TypeScript

import {
BadRequestException,
ConflictException,
NotFoundException,
} from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import type { AuthUser } from '../../../common/auth/auth-user';
import { DateTime } from '../../../common/value-objects/date-time/date-time';
import { Status } from '../../../common/value-objects/status/status';
import { BranchesService } from '../../configuration/branches/branches.service';
import { CustomersService } from '../../configuration/customers/customers.service';
import { EmployeesService } from '../../configuration/employees/employees.service';
import { PrivilegesService } from '../../privileges/privileges.service';
import type { Cycle } from './cycle';
import { CyclesRepository } from './cycles.repository';
import { CyclesService } from './cycles.service';
describe('CyclesService', () => {
let service: CyclesService;
let repository: jest.Mocked<
Pick<
CyclesRepository,
| 'list'
| 'findById'
| 'findLiveByKey'
| 'create'
| 'update'
| 'updateStatus'
| 'bulkUpdateStatus'
| 'archive'
| 'bulkArchive'
>
>;
const employeesService = { findById: jest.fn(), findByCode: jest.fn() };
const branchesService = { findById: jest.fn(), findByCode: jest.fn() };
const customersService = { findById: jest.fn(), findByCode: jest.fn() };
const privilegesService = { checkPermission: jest.fn(), checkAnyPermission: jest.fn() };
const now = DateTime.fromUnixMs(1_700_000_000_000);
const user: AuthUser = {
id: 'user-1',
username: 'alice',
jti: 'jti-1',
isSuperadmin: true,
};
const geometry = {
type: 'LineString' as const,
coordinates: [
[106.8, -6.2],
[106.9, -6.3],
[107.0, -6.4],
] as const,
};
const sample: Cycle = {
id: 'cyc-1',
employeeId: 'emp-1',
purpose: 'sales',
cycleNumber: 1,
weekdays: [
{
id: 'wd-1',
weekday: 'monday',
startBranchId: 'br-1',
endBranchId: 'br-2',
startBranch: { id: 'br-1', code: 'B1', name: 'Start' },
endBranch: { id: 'br-2', code: 'B2', name: 'End' },
routeGeometry: geometry,
destinations: [
{
id: 'dest-1',
customerId: 'cus-1',
customer: { id: 'cus-1', code: 'C1', name: 'Acme' },
sortOrder: 0,
},
],
},
],
status: Status.create('draft'),
createdAt: now,
updatedAt: now,
createdBy: 'user-1',
updatedBy: 'user-1',
employee: { id: 'emp-1', code: 'E1', name: 'Ada' },
createdByUser: { id: 'user-1', username: 'admin' },
updatedByUser: { id: 'user-1', username: 'admin' },
};
const located = { id: 'x', latitude: -6.2, longitude: 106.8 };
beforeEach(async () => {
jest.clearAllMocks();
repository = {
list: jest.fn(),
findById: jest.fn(),
findLiveByKey: jest.fn(),
create: jest.fn(),
update: jest.fn(),
updateStatus: jest.fn(),
bulkUpdateStatus: jest.fn(),
archive: jest.fn(),
bulkArchive: jest.fn(),
};
employeesService.findById.mockResolvedValue({ id: 'emp-1' });
branchesService.findById.mockResolvedValue(located);
customersService.findById.mockResolvedValue(located);
repository.findLiveByKey.mockResolvedValue(null);
repository.create.mockResolvedValue(sample);
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [
CyclesService,
{ provide: CyclesRepository, useValue: repository },
{ provide: EmployeesService, useValue: employeesService },
{ provide: BranchesService, useValue: branchesService },
{ provide: CustomersService, useValue: customersService },
{ provide: PrivilegesService, useValue: privilegesService },
],
}).compile();
service = moduleRef.get(CyclesService);
});
it('create rejects incomplete weekdays', async () => {
await expect(
service.create({
employeeId: 'emp-1',
purpose: 'sales',
cycleNumber: 1,
weekdays: {
monday: {
startBranchId: 'br-1',
endBranchId: 'br-2',
customerIds: [],
},
},
userId: 'user-1',
}),
).rejects.toBeInstanceOf(BadRequestException);
});
it('create rejects duplicate live keys', async () => {
repository.findLiveByKey.mockResolvedValue(sample);
await expect(
service.create({
employeeId: 'emp-1',
purpose: 'sales',
cycleNumber: 1,
weekdays: {
monday: {
startBranchId: 'br-1',
endBranchId: 'br-2',
customerIds: ['cus-1'],
},
},
userId: 'user-1',
}),
).rejects.toBeInstanceOf(ConflictException);
});
it('create persists a complete weekday and builds geometry', async () => {
await service.create({
employeeId: 'emp-1',
purpose: 'sales',
cycleNumber: 1,
weekdays: {
monday: {
startBranchId: 'br-1',
endBranchId: 'br-2',
customerIds: ['cus-1'],
},
},
userId: 'user-1',
});
expect(repository.create).toHaveBeenCalled();
const arg = repository.create.mock.calls[0][0];
expect(arg.weekdays[0].weekday).toBe('monday');
expect(arg.weekdays[0].routeGeometry.type).toBe('LineString');
});
it('delete archives instead of hard-deleting', async () => {
repository.findById.mockResolvedValue(sample);
repository.archive.mockResolvedValue(undefined);
await service.delete('cyc-1', user);
expect(repository.archive).toHaveBeenCalledWith('cyc-1', 'user-1');
});
it('findById throws when missing', async () => {
repository.findById.mockResolvedValue(null);
await expect(service.findById('missing', user)).rejects.toBeInstanceOf(
NotFoundException,
);
});
});