690522:0554 227 #01
CI / CD Pipeline / build (push) Successful in 5m25s
CI / CD Pipeline / deploy (push) Successful in 8m59s

This commit is contained in:
2026-05-22 05:54:34 +07:00
parent a2952a32a4
commit f47363c24a
15 changed files with 2653 additions and 0 deletions
@@ -0,0 +1,122 @@
// File: src/modules/response-code/services/audit.service.spec.ts
// Change Log:
// - 2026-05-21: เพิ่ม unit tests สำหรับ ResponseCodeAuditService
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ResponseCodeAuditService } from './audit.service';
import { AuditLog } from '../../../common/entities/audit-log.entity';
describe('ResponseCodeAuditService', () => {
let service: ResponseCodeAuditService;
const mockAuditLog: Partial<AuditLog> = {
userId: 1,
action: 'response_code.change',
severity: 'INFO',
entityType: 'review_task',
entityId: 'task-uuid-001',
detailsJson: {},
};
const mockAuditLogRepo = {
create: jest.fn().mockReturnValue(mockAuditLog),
save: jest.fn().mockResolvedValue(mockAuditLog),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ResponseCodeAuditService,
{
provide: getRepositoryToken(AuditLog),
useValue: mockAuditLogRepo,
},
],
}).compile();
service = module.get<ResponseCodeAuditService>(ResponseCodeAuditService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('logReviewTaskResponseCodeChange', () => {
it('ควรบันทึก audit log พร้อมข้อมูลครบถ้วน (Happy Path)', async () => {
await service.logReviewTaskResponseCodeChange({
reviewTaskPublicId: 'task-uuid-001',
responseCodePublicId: 'rc-uuid-001',
previousResponseCodeId: 1,
currentResponseCodeId: 2,
comments: 'Changed from 1A to 2',
userId: 10,
});
expect(mockAuditLogRepo.create).toHaveBeenCalledWith({
userId: 10,
action: 'response_code.change',
severity: 'INFO',
entityType: 'review_task',
entityId: 'task-uuid-001',
detailsJson: {
previousResponseCodeId: 1,
currentResponseCodeId: 2,
responseCodePublicId: 'rc-uuid-001',
comments: 'Changed from 1A to 2',
},
});
expect(mockAuditLogRepo.save).toHaveBeenCalledTimes(1);
});
it('ควร default userId เป็น null เมื่อไม่ระบุ', async () => {
await service.logReviewTaskResponseCodeChange({
reviewTaskPublicId: 'task-uuid-002',
responseCodePublicId: 'rc-uuid-002',
currentResponseCodeId: 3,
});
expect(mockAuditLogRepo.create).toHaveBeenCalledWith(
expect.objectContaining({ userId: null })
);
});
it('ควร default previousResponseCodeId เป็น null เมื่อไม่ระบุ', async () => {
await service.logReviewTaskResponseCodeChange({
reviewTaskPublicId: 'task-uuid-003',
responseCodePublicId: 'rc-uuid-003',
currentResponseCodeId: 1,
});
expect(mockAuditLogRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
detailsJson: expect.objectContaining({
previousResponseCodeId: null,
}),
})
);
});
it('ควร default comments เป็น null เมื่อไม่ระบุ', async () => {
await service.logReviewTaskResponseCodeChange({
reviewTaskPublicId: 'task-uuid-004',
responseCodePublicId: 'rc-uuid-004',
currentResponseCodeId: 2,
});
expect(mockAuditLogRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
detailsJson: expect.objectContaining({ comments: null }),
})
);
});
it('ควร throw เมื่อ repo.save ล้มเหลว', async () => {
mockAuditLogRepo.save.mockRejectedValueOnce(new Error('DB Error'));
await expect(
service.logReviewTaskResponseCodeChange({
reviewTaskPublicId: 'task-uuid-005',
responseCodePublicId: 'rc-uuid-005',
currentResponseCodeId: 1,
})
).rejects.toThrow('DB Error');
});
});
});
@@ -0,0 +1,180 @@
// File: src/modules/response-code/services/implications.service.spec.ts
// Change Log:
// - 2026-05-21: เพิ่ม unit tests สำหรับ ImplicationsService (FR-007)
import { Test, TestingModule } from '@nestjs/testing';
import { ImplicationsService } from './implications.service';
import { ResponseCode } from '../entities/response-code.entity';
// Helper สร้าง mock ResponseCode
const makeCode = (
code: string,
overrides: Partial<ResponseCode> = {}
): ResponseCode =>
({
id: 1,
code,
descriptionTh: 'ทดสอบ',
descriptionEn: 'Test',
category: 'ENGINEERING',
isSystem: false,
isActive: true,
implications: {},
notifyRoles: [],
...overrides,
}) as unknown as ResponseCode;
describe('ImplicationsService', () => {
let service: ImplicationsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ImplicationsService],
}).compile();
service = module.get<ImplicationsService>(ImplicationsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('evaluate — severity', () => {
it('ควรคืน CRITICAL เมื่อ code=3 (Rejected)', () => {
const result = service.evaluate(makeCode('3'));
expect(result.severity).toBe('CRITICAL');
});
it('ควรคืน HIGH เมื่อ code=1C', () => {
const result = service.evaluate(makeCode('1C'));
expect(result.severity).toBe('HIGH');
});
it('ควรคืน HIGH เมื่อ code=1D', () => {
const result = service.evaluate(makeCode('1D'));
expect(result.severity).toBe('HIGH');
});
it('ควรคืน HIGH เมื่อ affectsSchedule=true และ affectsCost=true', () => {
const result = service.evaluate(
makeCode('2', {
implications: { affectsSchedule: true, affectsCost: true },
} as Partial<ResponseCode>)
);
expect(result.severity).toBe('HIGH');
});
it('ควรคืน MEDIUM เมื่อ requiresContractReview=true', () => {
const result = service.evaluate(
makeCode('1B', {
implications: { requiresContractReview: true },
} as Partial<ResponseCode>)
);
expect(result.severity).toBe('MEDIUM');
});
it('ควรคืน MEDIUM เมื่อ affectsSchedule=true', () => {
const result = service.evaluate(
makeCode('1B', {
implications: { affectsSchedule: true },
} as Partial<ResponseCode>)
);
expect(result.severity).toBe('MEDIUM');
});
it('ควรคืน MEDIUM เมื่อ affectsCost=true', () => {
const result = service.evaluate(
makeCode('1B', {
implications: { affectsCost: true },
} as Partial<ResponseCode>)
);
expect(result.severity).toBe('MEDIUM');
});
it('ควรคืน LOW เมื่อไม่มีผลกระทบใดๆ', () => {
const result = service.evaluate(makeCode('1A'));
expect(result.severity).toBe('LOW');
});
});
describe('evaluate — actionRequired', () => {
it('ควรเพิ่ม action สำหรับ code=3', () => {
const result = service.evaluate(makeCode('3'));
expect(result.actionRequired).toContain(
'Document rejected — originator must revise and resubmit'
);
});
it('ควรเพิ่ม action สำหรับ requiresContractReview', () => {
const result = service.evaluate(
makeCode('1C', {
implications: { requiresContractReview: true },
} as Partial<ResponseCode>)
);
expect(result.actionRequired).toContain(
'Contract review required — notify Contract Manager'
);
});
it('ควรเพิ่ม action สำหรับ affectsCost', () => {
const result = service.evaluate(
makeCode('1B', {
implications: { affectsCost: true },
} as Partial<ResponseCode>)
);
expect(result.actionRequired).toContain(
'Cost impact assessment required — notify QS Manager'
);
});
it('ควรเพิ่ม action สำหรับ requiresEiaAmendment', () => {
const result = service.evaluate(
makeCode('1B', {
implications: { requiresEiaAmendment: true },
} as Partial<ResponseCode>)
);
expect(result.actionRequired).toContain(
'EIA amendment may be required — notify EIA Officer'
);
});
it('ควรเพิ่ม action สำหรับ code=2', () => {
const result = service.evaluate(makeCode('2'));
expect(result.actionRequired).toContain(
'Minor comments — originator to revise and resubmit'
);
});
it('ควรคืน actionRequired ว่างเมื่อ code=1A ไม่มี implications', () => {
const result = service.evaluate(makeCode('1A'));
expect(result.actionRequired).toHaveLength(0);
});
});
describe('evaluate — flags', () => {
it('ควรคืน affectsSchedule=true จาก implications', () => {
const result = service.evaluate(
makeCode('1B', {
implications: { affectsSchedule: true },
} as Partial<ResponseCode>)
);
expect(result.affectsSchedule).toBe(true);
});
it('ควร default ทุก flag เป็น false เมื่อ implications ว่าง', () => {
const result = service.evaluate(makeCode('1A'));
expect(result.affectsSchedule).toBe(false);
expect(result.affectsCost).toBe(false);
expect(result.requiresContractReview).toBe(false);
expect(result.requiresEiaAmendment).toBe(false);
});
it('ควรคืน notifyRoles จาก responseCode', () => {
const result = service.evaluate(
makeCode('3', {
notifyRoles: ['CONTRACT_MANAGER', 'QS_MANAGER'],
} as Partial<ResponseCode>)
);
expect(result.notifyRoles).toEqual(['CONTRACT_MANAGER', 'QS_MANAGER']);
});
});
});
@@ -0,0 +1,122 @@
// File: src/modules/response-code/services/inheritance.service.spec.ts
// Change Log:
// - 2026-05-21: เพิ่ม unit tests สำหรับ InheritanceService (T062, FR-021)
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { InheritanceService } from './inheritance.service';
import { ResponseCodeRule } from '../entities/response-code-rule.entity';
// Helper สร้าง mock rule
const makeRule = (
id: number,
responseCodeId: number,
publicId: string,
projectId?: number,
overrides: Record<string, unknown> = {}
): Partial<ResponseCodeRule> => ({
id,
responseCodeId,
documentTypeId: 1,
projectId,
isEnabled: true,
requiresComments: false,
triggersNotification: false,
responseCode: { publicId } as unknown as ResponseCodeRule['responseCode'],
...overrides,
});
describe('InheritanceService', () => {
let service: InheritanceService;
const mockRuleRepo = {
find: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
InheritanceService,
{
provide: getRepositoryToken(ResponseCodeRule),
useValue: mockRuleRepo,
},
],
}).compile();
service = module.get<InheritanceService>(InheritanceService);
});
afterEach(() => jest.clearAllMocks());
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('resolveMatrix — global only', () => {
it('ควรคืน global rules เมื่อไม่ระบุ projectId', async () => {
const globalRules = [makeRule(1, 10, 'rc-1A'), makeRule(2, 20, 'rc-2')];
mockRuleRepo.find.mockResolvedValueOnce(globalRules);
const result = await service.resolveMatrix(1);
expect(result).toHaveLength(2);
expect(result[0].isOverridden).toBe(false);
expect(result[0].responseCodePublicId).toBe('rc-1A');
});
it('ควรคืน array ว่างเมื่อไม่มี global rules', async () => {
mockRuleRepo.find.mockResolvedValueOnce([]);
const result = await service.resolveMatrix(99);
expect(result).toHaveLength(0);
});
});
describe('resolveMatrix — with project overrides', () => {
it('ควร merge: project rule ชนะ global rule ของ responseCode เดียวกัน', async () => {
const globalRules = [makeRule(1, 10, 'rc-1A')];
const projectRules = [
makeRule(2, 10, 'rc-1A-override', 5, {
isEnabled: false,
requiresComments: true,
}),
];
// เรียก find สองครั้ง: global, project
mockRuleRepo.find
.mockResolvedValueOnce(globalRules)
.mockResolvedValueOnce(projectRules);
const result = await service.resolveMatrix(1, 5);
expect(result).toHaveLength(1);
expect(result[0].isOverridden).toBe(true);
expect(result[0].isEnabled).toBe(false);
expect(result[0].requiresComments).toBe(true);
expect(result[0].parentRuleId).toBe(1); // global rule id
});
it('ควรใช้ global rule เมื่อ project ไม่ override', async () => {
const globalRules = [makeRule(1, 10, 'rc-1A'), makeRule(2, 20, 'rc-2')];
const projectRules: Partial<ResponseCodeRule>[] = []; // ไม่มี override
mockRuleRepo.find
.mockResolvedValueOnce(globalRules)
.mockResolvedValueOnce(projectRules);
const result = await service.resolveMatrix(1, 5);
expect(result).toHaveLength(2);
expect(result[0].isOverridden).toBe(false);
expect(result[0].parentRuleId).toBeUndefined();
});
it('ควรเพิ่ม project-only rule ที่ไม่มี global parent', async () => {
const globalRules = [makeRule(1, 10, 'rc-1A')];
const projectRules = [
makeRule(1, 10, 'rc-1A'), // overlap กับ global
makeRule(3, 30, 'rc-extra', 5), // project-only (responseCodeId=30 ไม่มีใน global)
];
mockRuleRepo.find
.mockResolvedValueOnce(globalRules)
.mockResolvedValueOnce(projectRules);
const result = await service.resolveMatrix(1, 5);
// 1 merged + 1 project-only = 2
expect(result).toHaveLength(2);
const extra = result.find((r) => r.responseCodeId === 30);
expect(extra).toBeDefined();
expect(extra?.isOverridden).toBe(true);
expect(extra?.parentRuleId).toBeUndefined();
});
});
});
@@ -0,0 +1,193 @@
// File: src/modules/response-code/services/matrix-management.service.spec.ts
// Change Log:
// - 2026-05-21: เพิ่ม unit tests สำหรับ MatrixManagementService (T061, FR-022)
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { MatrixManagementService } from './matrix-management.service';
import { ResponseCodeRule } from '../entities/response-code-rule.entity';
import { ResponseCode } from '../entities/response-code.entity';
const mockCode = {
id: 1,
publicId: 'rc-uuid-1A',
code: '1A',
isSystem: false,
};
const mockSystemCode = { id: 2, publicId: 'rc-sys', code: '0', isSystem: true };
const mockExistingRule = {
id: 10,
publicId: 'rule-uuid-001',
documentTypeId: 1,
responseCodeId: 1,
projectId: undefined,
isEnabled: true,
requiresComments: false,
triggersNotification: false,
};
describe('MatrixManagementService', () => {
let service: MatrixManagementService;
const mockRuleRepo = {
findOne: jest.fn(),
find: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
};
const mockCodeRepo = {
findOne: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MatrixManagementService,
{
provide: getRepositoryToken(ResponseCodeRule),
useValue: mockRuleRepo,
},
{
provide: getRepositoryToken(ResponseCode),
useValue: mockCodeRepo,
},
],
}).compile();
service = module.get<MatrixManagementService>(MatrixManagementService);
});
afterEach(() => jest.clearAllMocks());
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('upsertRule', () => {
it('ควร throw NotFoundException เมื่อ ResponseCode ไม่พบ', async () => {
mockCodeRepo.findOne.mockResolvedValueOnce(null);
await expect(
service.upsertRule({
documentTypeId: 1,
responseCodePublicId: 'not-found',
isEnabled: true,
})
).rejects.toThrow(NotFoundException);
});
it('ควร throw BadRequestException เมื่อพยายาม disable system code', async () => {
mockCodeRepo.findOne.mockResolvedValueOnce(mockSystemCode);
await expect(
service.upsertRule({
documentTypeId: 1,
responseCodePublicId: 'rc-sys',
isEnabled: false,
})
).rejects.toThrow(BadRequestException);
});
it('ควรอัปเดต existing rule (isEnabled, requiresComments)', async () => {
mockCodeRepo.findOne.mockResolvedValueOnce(mockCode);
mockRuleRepo.findOne.mockResolvedValueOnce({ ...mockExistingRule });
mockRuleRepo.save.mockResolvedValueOnce({
...mockExistingRule,
isEnabled: false,
});
const result = await service.upsertRule({
documentTypeId: 1,
responseCodePublicId: 'rc-uuid-1A',
isEnabled: false,
requiresComments: true,
});
expect(mockRuleRepo.save).toHaveBeenCalledTimes(1);
expect(result.isEnabled).toBe(false);
});
it('ควรสร้าง rule ใหม่เมื่อยังไม่มี', async () => {
mockCodeRepo.findOne.mockResolvedValueOnce(mockCode);
mockRuleRepo.findOne.mockResolvedValueOnce(null); // ไม่มี existing
const createdRule = {
documentTypeId: 1,
responseCodeId: 1,
isEnabled: true,
requiresComments: false,
triggersNotification: false,
};
mockRuleRepo.create.mockReturnValueOnce(createdRule);
mockRuleRepo.save.mockResolvedValueOnce(createdRule);
const result = await service.upsertRule({
documentTypeId: 1,
responseCodePublicId: 'rc-uuid-1A',
isEnabled: true,
});
expect(mockRuleRepo.create).toHaveBeenCalledTimes(1);
expect(result.isEnabled).toBe(true);
});
it('ควร default requiresComments=false และ triggersNotification=false เมื่อสร้างใหม่', async () => {
mockCodeRepo.findOne.mockResolvedValueOnce(mockCode);
mockRuleRepo.findOne.mockResolvedValueOnce(null);
mockRuleRepo.create.mockImplementation(
(v: Partial<ResponseCodeRule>) => v
);
mockRuleRepo.save.mockImplementation((v: Partial<ResponseCodeRule>) =>
Promise.resolve(v)
);
const result = await service.upsertRule({
documentTypeId: 1,
responseCodePublicId: 'rc-uuid-1A',
isEnabled: true,
});
expect(result.requiresComments).toBe(false);
expect(result.triggersNotification).toBe(false);
});
});
describe('getRulesByDocType', () => {
it('ควรดึง rules ของ documentType + projectId ที่ระบุ', async () => {
mockRuleRepo.find.mockResolvedValueOnce([mockExistingRule]);
const result = await service.getRulesByDocType(1, 5);
expect(mockRuleRepo.find).toHaveBeenCalledWith({
where: { documentTypeId: 1, projectId: 5 },
relations: ['responseCode'],
});
expect(result).toHaveLength(1);
});
it('ควรดึง global rules เมื่อไม่ระบุ projectId', async () => {
mockRuleRepo.find.mockResolvedValueOnce([mockExistingRule]);
await service.getRulesByDocType(1);
expect(mockRuleRepo.find).toHaveBeenCalledWith({
where: { documentTypeId: 1, projectId: undefined },
relations: ['responseCode'],
});
});
});
describe('deleteProjectOverride', () => {
it('ควร throw NotFoundException เมื่อ rule ไม่พบ', async () => {
mockRuleRepo.findOne.mockResolvedValueOnce(null);
await expect(
service.deleteProjectOverride('nonexistent-rule')
).rejects.toThrow(NotFoundException);
});
it('ควร throw BadRequestException เมื่อพยายามลบ global rule', async () => {
mockRuleRepo.findOne.mockResolvedValueOnce({
...mockExistingRule,
projectId: undefined,
});
await expect(
service.deleteProjectOverride('rule-uuid-001')
).rejects.toThrow(BadRequestException);
});
it('ควรลบ project override สำเร็จ', async () => {
const projectRule = { ...mockExistingRule, projectId: 5 };
mockRuleRepo.findOne.mockResolvedValueOnce(projectRule);
mockRuleRepo.remove.mockResolvedValueOnce(undefined);
await service.deleteProjectOverride('rule-uuid-001');
expect(mockRuleRepo.remove).toHaveBeenCalledWith(projectRule);
});
});
});
@@ -0,0 +1,140 @@
// File: src/modules/response-code/services/notification-trigger.service.spec.ts
// Change Log:
// - 2026-05-21: เพิ่ม unit tests สำหรับ NotificationTriggerService (FR-007)
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { NotificationTriggerService } from './notification-trigger.service';
import { ResponseCode } from '../entities/response-code.entity';
import { User } from '../../user/entities/user.entity';
import { NotificationService } from '../../notification/notification.service';
import { ImplicationsService } from './implications.service';
const mockResponseCode = {
id: 1,
publicId: 'rc-3',
code: '3',
descriptionEn: 'Rejected',
notifyRoles: ['CONTRACT_MANAGER'],
implications: {},
};
describe('NotificationTriggerService', () => {
let service: NotificationTriggerService;
const mockRcRepo = { findOne: jest.fn() };
const mockUserRepo = {
createQueryBuilder: jest.fn(),
};
const mockNotificationService = {
send: jest.fn().mockResolvedValue(undefined),
};
const mockImplicationsService = { evaluate: jest.fn() };
// Helper สำหรับ query builder chain
const makeQB = (users: Partial<User>[]) => ({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(users),
});
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
NotificationTriggerService,
{ provide: getRepositoryToken(ResponseCode), useValue: mockRcRepo },
{ provide: getRepositoryToken(User), useValue: mockUserRepo },
{ provide: NotificationService, useValue: mockNotificationService },
{ provide: ImplicationsService, useValue: mockImplicationsService },
],
}).compile();
service = module.get<NotificationTriggerService>(
NotificationTriggerService
);
});
afterEach(() => jest.clearAllMocks());
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('triggerIfRequired', () => {
it('ควร return ทันทีเมื่อ ResponseCode ไม่พบ (warn, no throw)', async () => {
mockRcRepo.findOne.mockResolvedValueOnce(null);
await expect(
service.triggerIfRequired('not-found', 'rfa-1', 'DOC-001', 1)
).resolves.not.toThrow();
expect(mockNotificationService.send).not.toHaveBeenCalled();
});
it('ควร return ทันทีเมื่อ severity=LOW', async () => {
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
mockImplicationsService.evaluate.mockReturnValueOnce({
severity: 'LOW',
notifyRoles: [],
actionRequired: [],
});
await service.triggerIfRequired('rc-3', 'rfa-1', 'DOC-001', 1);
expect(mockNotificationService.send).not.toHaveBeenCalled();
});
it('ควร return ทันทีเมื่อ notifyRoles ว่าง (severity != LOW)', async () => {
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
mockImplicationsService.evaluate.mockReturnValueOnce({
severity: 'CRITICAL',
notifyRoles: [],
actionRequired: [],
});
await service.triggerIfRequired('rc-3', 'rfa-1', 'DOC-001', 1);
expect(mockNotificationService.send).not.toHaveBeenCalled();
});
it('ควรส่งแจ้งเตือนถึง user ที่มี role ที่เกี่ยวข้อง', async () => {
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
mockImplicationsService.evaluate.mockReturnValueOnce({
severity: 'CRITICAL',
notifyRoles: ['CONTRACT_MANAGER'],
actionRequired: ['Contract review required'],
});
const targetUser = { user_id: 99 } as User;
const qb = makeQB([targetUser]);
mockUserRepo.createQueryBuilder.mockReturnValueOnce(qb);
await service.triggerIfRequired('rc-3', 'rfa-001', 'DOC-001', 1);
expect(mockNotificationService.send).toHaveBeenCalledTimes(1);
expect(mockNotificationService.send).toHaveBeenCalledWith(
expect.objectContaining({
userId: 99,
type: 'SYSTEM',
entityType: 'rfa',
})
);
});
it('ควรส่งแจ้งเตือนแบบ parallel ถึงหลาย users', async () => {
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
mockImplicationsService.evaluate.mockReturnValueOnce({
severity: 'HIGH',
notifyRoles: ['CONTRACT_MANAGER'],
actionRequired: [],
});
const users = [{ user_id: 1 }, { user_id: 2 }, { user_id: 3 }] as User[];
const qb = makeQB(users);
mockUserRepo.createQueryBuilder.mockReturnValueOnce(qb);
await service.triggerIfRequired('rc-1C', 'rfa-002', 'DOC-002', 5);
expect(mockNotificationService.send).toHaveBeenCalledTimes(3);
});
it('ควร return ทันทีเมื่อไม่พบ users ที่ match roles', async () => {
mockRcRepo.findOne.mockResolvedValueOnce(mockResponseCode);
mockImplicationsService.evaluate.mockReturnValueOnce({
severity: 'HIGH',
notifyRoles: ['CONTRACT_MANAGER'],
actionRequired: [],
});
const qb = makeQB([]); // ไม่มี users
mockUserRepo.createQueryBuilder.mockReturnValueOnce(qb);
await service.triggerIfRequired('rc-1C', 'rfa-003', 'DOC-003', 5);
expect(mockNotificationService.send).not.toHaveBeenCalled();
});
});
});