- 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.
193 lines
5.8 KiB
TypeScript
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,
|
|
);
|
|
});
|
|
});
|