Files
lcbp3/backend/src/modules/correspondence/correspondence.service.spec.ts
T
admin 7080a37a82
CI / CD Pipeline / build (push) Failing after 23m26s
CI / CD Pipeline / deploy (push) Has been skipped
260330:1327 Addied correspondence_revieion_attcahments table table #02
2026-03-30 13:27:38 +07:00

807 lines
25 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { ForbiddenException } from '@nestjs/common';
import { CorrespondenceService } from './correspondence.service';
import { Correspondence } from './entities/correspondence.entity';
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
import { CorrespondenceType } from './entities/correspondence-type.entity';
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
import { CorrespondenceTag } from './entities/correspondence-tag.entity';
import { Organization } from '../organization/entities/organization.entity';
import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';
import { CorrespondenceRevisionAttachment } from './entities/correspondence-revision-attachment.entity';
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
import { JsonSchemaService } from '../json-schema/json-schema.service';
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
import { UserService } from '../user/user.service';
import { SearchService } from '../search/search.service';
import { FileStorageService } from '../../common/file-storage/file-storage.service';
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
import { NotificationService } from '../notification/notification.service';
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
import { User } from '../user/entities/user.entity';
describe('CorrespondenceService', () => {
let service: CorrespondenceService;
let numberingService: DocumentNumberingService;
let correspondenceRepo: Repository<Correspondence>;
let revisionRepo: Repository<CorrespondenceRevision>;
let testingModule: TestingModule;
let _dataSource: DataSource;
const createMockRepository = () => ({
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
softDelete: jest.fn(),
createQueryBuilder: jest.fn(() => ({
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
take: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(null),
getMany: jest.fn().mockResolvedValue([]),
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
})),
});
const mockDataSource = {
createQueryRunner: jest.fn(() => ({
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
},
})),
getRepository: jest.fn(() => createMockRepository()),
manager: {
findOne: jest.fn(),
},
};
beforeEach(async () => {
testingModule = await Test.createTestingModule({
providers: [
CorrespondenceService,
{
provide: getRepositoryToken(Correspondence),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceRevision),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceType),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceStatus),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceReference),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceTag),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(Organization),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(CorrespondenceRecipient),
useValue: createMockRepository(),
},
{
provide: DocumentNumberingService,
useValue: {
generateNextNumber: jest.fn(),
updateNumberForDraft: jest.fn(),
previewNextNumber: jest.fn(),
},
},
{
provide: JsonSchemaService,
useValue: { validate: jest.fn() },
},
{
provide: WorkflowEngineService,
useValue: { createInstance: jest.fn() },
},
{
provide: UserService,
useValue: {
findOne: jest.fn(),
getUserPermissions: jest.fn().mockResolvedValue([]),
},
},
{
provide: DataSource,
useValue: mockDataSource,
},
{
provide: SearchService,
useValue: { indexDocument: jest.fn() },
},
{
provide: FileStorageService,
useValue: { commit: jest.fn().mockResolvedValue([]) },
},
{
provide: UuidResolverService,
useValue: {
resolveProjectId: jest.fn().mockResolvedValue(1),
resolveOrganizationId: jest.fn().mockResolvedValue(1),
},
},
{
provide: NotificationService,
useValue: { send: jest.fn().mockResolvedValue(undefined) },
},
{
provide: getRepositoryToken(CorrespondenceRevisionAttachment),
useValue: createMockRepository(),
},
],
}).compile();
service = testingModule.get<CorrespondenceService>(CorrespondenceService);
numberingService = testingModule.get<DocumentNumberingService>(
DocumentNumberingService
);
correspondenceRepo = testingModule.get<Repository<Correspondence>>(
getRepositoryToken(Correspondence)
);
revisionRepo = testingModule.get<Repository<CorrespondenceRevision>>(
getRepositoryToken(CorrespondenceRevision)
);
_dataSource = testingModule.get<DataSource>(DataSource);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('update', () => {
it('should allow non-draft update for org-admin+ permissions', async () => {
const mockUser = {
user_id: 1,
primaryOrganizationId: 10,
} as unknown as User;
const mockRevision = {
id: 100,
correspondenceId: 1,
isCurrent: true,
statusId: 23,
};
jest
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(mockRevision as unknown as CorrespondenceRevision);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 23,
statusCode: 'SUBOWN',
});
const userService = testingModule.get<UserService>(UserService);
(userService.getUserPermissions as jest.Mock).mockResolvedValue([
'correspondence.cancel',
]);
jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue({
id: 1,
publicId: 'corr-uuid-1',
correspondenceNumber: 'CORR-001',
projectId: 1,
createdAt: new Date(),
revisions: [],
} as unknown as Correspondence);
await expect(
service.update(1, { subject: 'Updated Subject' }, mockUser)
).resolves.toBeDefined();
});
it('should reject non-draft update for non-admin permissions', async () => {
const mockUser = {
user_id: 2,
primaryOrganizationId: 10,
} as unknown as User;
const mockRevision = {
id: 101,
correspondenceId: 2,
isCurrent: true,
statusId: 23,
};
jest
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(mockRevision as unknown as CorrespondenceRevision);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 23,
statusCode: 'SUBOWN',
});
const userService = testingModule.get<UserService>(UserService);
(userService.getUserPermissions as jest.Mock).mockResolvedValue([
'correspondence.edit',
]);
await expect(
service.update(2, { subject: 'Should Fail' }, mockUser)
).rejects.toThrow(ForbiddenException);
});
it('should NOT regenerate number if critical fields unchanged', async () => {
const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User;
const mockRevision = {
id: 100,
correspondenceId: 1,
isCurrent: true,
statusId: 5,
};
jest
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(mockRevision as unknown as CorrespondenceRevision);
const mockCorr = {
id: 1,
projectId: 1,
correspondenceTypeId: 2,
disciplineId: 3,
originatorId: 10,
correspondenceNumber: 'OLD-NUM',
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
};
jest
.spyOn(correspondenceRepo, 'findOne')
.mockResolvedValue(mockCorr as unknown as Correspondence);
const updateDto: UpdateCorrespondenceDto = {
projectId: 1,
disciplineId: 3,
};
await service.update(1, updateDto, mockUser);
expect(
numberingService.updateNumberForDraft as jest.Mock
).not.toHaveBeenCalled();
});
it('should regenerate number if Project ID changes', async () => {
const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User;
const mockRevision = {
id: 100,
correspondenceId: 1,
isCurrent: true,
statusId: 5,
};
jest
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(mockRevision as unknown as CorrespondenceRevision);
const mockCorr = {
id: 1,
projectId: 1,
correspondenceTypeId: 2,
disciplineId: 3,
originatorId: 10,
correspondenceNumber: 'OLD-NUM',
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
};
jest
.spyOn(correspondenceRepo, 'findOne')
.mockResolvedValue(mockCorr as unknown as Correspondence);
const updateDto: UpdateCorrespondenceDto = {
projectId: 2,
};
const uuidResolver =
testingModule.get<UuidResolverService>(UuidResolverService);
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(2);
await service.update(1, updateDto, mockUser);
expect(
numberingService.updateNumberForDraft as jest.Mock
).toHaveBeenCalled();
});
it('should regenerate number if Document Type changes', async () => {
const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User;
const mockRevision = {
id: 100,
correspondenceId: 1,
isCurrent: true,
statusId: 5,
};
jest
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(mockRevision as unknown as CorrespondenceRevision);
const mockCorr = {
id: 1,
projectId: 1,
correspondenceTypeId: 2,
disciplineId: 3,
originatorId: 10,
correspondenceNumber: 'OLD-NUM',
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
};
jest
.spyOn(correspondenceRepo, 'findOne')
.mockResolvedValue(mockCorr as unknown as Correspondence);
const updateDto: UpdateCorrespondenceDto = {
typeId: 999,
};
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 999,
typeCode: 'NEW-TYPE',
});
await service.update(1, updateDto, mockUser);
expect(
numberingService.updateNumberForDraft as jest.Mock
).toHaveBeenCalled();
});
it('should regenerate number if Recipient Organization changes', async () => {
const mockUser = { id: 1, primaryOrganizationId: 10 } as unknown as User;
const mockRevision = {
id: 100,
correspondenceId: 1,
isCurrent: true,
statusId: 5,
};
jest
.spyOn(revisionRepo, 'findOne')
.mockResolvedValue(mockRevision as unknown as CorrespondenceRevision);
const mockCorr = {
id: 1,
projectId: 1,
correspondenceTypeId: 2,
disciplineId: 3,
originatorId: 10,
correspondenceNumber: 'OLD-NUM',
recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],
};
jest
.spyOn(correspondenceRepo, 'findOne')
.mockResolvedValue(mockCorr as unknown as Correspondence);
// Access DataSource manager for mocking
mockDataSource.manager.findOne.mockResolvedValue({
id: 88,
organizationCode: 'NEW-ORG',
} as unknown as Organization);
const updateDto: UpdateCorrespondenceDto = {
recipients: [{ type: 'TO', organizationId: 88 }],
};
await service.update(1, updateDto, mockUser);
expect(
numberingService.updateNumberForDraft as jest.Mock
).toHaveBeenCalled();
});
});
describe('create', () => {
it('should allow system.manage_all user without primaryOrganizationId when originatorId is provided', async () => {
const mockUser = {
user_id: 1,
primaryOrganizationId: null,
} as unknown as User;
const createDto: CreateCorrespondenceDto = {
projectId: 'project-uuid',
typeId: 1,
subject: 'Test Subject',
originatorId: 'originator-uuid',
recipients: [{ organizationId: 'recipient-uuid', type: 'TO' }],
};
const userService = testingModule.get<UserService>(UserService);
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
const uuidResolver =
testingModule.get<UuidResolverService>(UuidResolverService);
(userService.findOne as jest.Mock).mockResolvedValue({
user_id: 1,
primaryOrganizationId: null,
});
(userService.getUserPermissions as jest.Mock).mockResolvedValue([
'system.manage_all',
]);
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(100);
(uuidResolver.resolveOrganizationId as jest.Mock).mockImplementation(
(value: number | string) => {
if (value === 'originator-uuid') return 10;
if (value === 'recipient-uuid') return 20;
return 0;
}
);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
typeCode: 'LTR',
});
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
statusCode: 'DRAFT',
});
(numberingService.generateNextNumber as jest.Mock).mockResolvedValue({
number: 'DOC-001',
});
mockDataSource.manager.findOne
.mockResolvedValueOnce({ id: 10, organizationCode: 'ORG' })
.mockResolvedValueOnce({ id: 20, organizationCode: 'REC' });
const queryRunner = {
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(
(_entity: unknown, payload: Record<string, unknown>) => payload
),
save: jest
.fn()
.mockResolvedValueOnce({ id: 999, publicId: 'corr-uuid' })
.mockResolvedValueOnce({ id: 1000 })
.mockResolvedValueOnce([]),
findOne: jest.fn(),
},
};
(mockDataSource.createQueryRunner as jest.Mock).mockReturnValue(
queryRunner
);
await service.create(createDto, mockUser);
expect(queryRunner.manager.create).toHaveBeenCalledWith(
Correspondence,
expect.objectContaining({ originatorId: 10 })
);
});
it('should set revisionLabel to "A" for RFA type', async () => {
const mockUser = {
user_id: 1,
primaryOrganizationId: 10,
} as unknown as User;
const createDto: CreateCorrespondenceDto = {
projectId: 'project-uuid',
typeId: 1,
subject: 'Test Subject',
recipients: [{ organizationId: 'recipient-uuid', type: 'TO' }],
};
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
const uuidResolver =
testingModule.get<UuidResolverService>(UuidResolverService);
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(100);
(uuidResolver.resolveOrganizationId as jest.Mock).mockResolvedValue(20);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
typeCode: 'RFA',
});
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
statusCode: 'DRAFT',
});
(numberingService.generateNextNumber as jest.Mock).mockResolvedValue({
number: 'DOC-001',
});
mockDataSource.manager.findOne
.mockResolvedValueOnce({ id: 10, organizationCode: 'ORG' })
.mockResolvedValueOnce({ id: 20, organizationCode: 'REC' });
const queryRunner = {
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(
(_entity: unknown, payload: Record<string, unknown>) => payload
),
save: jest
.fn()
.mockResolvedValueOnce({ id: 999, publicId: 'corr-uuid' })
.mockResolvedValueOnce({ id: 1000 })
.mockResolvedValueOnce([]),
findOne: jest.fn(),
},
};
(mockDataSource.createQueryRunner as jest.Mock).mockReturnValue(
queryRunner
);
await service.create(createDto, mockUser);
expect(queryRunner.manager.create).toHaveBeenCalledWith(
CorrespondenceRevision,
expect.objectContaining({ revisionLabel: 'A' })
);
});
it('should set revisionLabel to "A" for RFI type', async () => {
const mockUser = {
user_id: 1,
primaryOrganizationId: 10,
} as unknown as User;
const createDto: CreateCorrespondenceDto = {
projectId: 'project-uuid',
typeId: 1,
subject: 'Test Subject',
recipients: [{ organizationId: 'recipient-uuid', type: 'TO' }],
};
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
const uuidResolver =
testingModule.get<UuidResolverService>(UuidResolverService);
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(100);
(uuidResolver.resolveOrganizationId as jest.Mock).mockResolvedValue(20);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
typeCode: 'RFI',
});
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
statusCode: 'DRAFT',
});
(numberingService.generateNextNumber as jest.Mock).mockResolvedValue({
number: 'DOC-001',
});
mockDataSource.manager.findOne
.mockResolvedValueOnce({ id: 10, organizationCode: 'ORG' })
.mockResolvedValueOnce({ id: 20, organizationCode: 'REC' });
const queryRunner = {
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(
(_entity: unknown, payload: Record<string, unknown>) => payload
),
save: jest
.fn()
.mockResolvedValueOnce({ id: 999, publicId: 'corr-uuid' })
.mockResolvedValueOnce({ id: 1000 })
.mockResolvedValueOnce([]),
findOne: jest.fn(),
},
};
(mockDataSource.createQueryRunner as jest.Mock).mockReturnValue(
queryRunner
);
await service.create(createDto, mockUser);
expect(queryRunner.manager.create).toHaveBeenCalledWith(
CorrespondenceRevision,
expect.objectContaining({ revisionLabel: 'A' })
);
});
it('should set revisionLabel to null for LETTER type', async () => {
const mockUser = {
user_id: 1,
primaryOrganizationId: 10,
} as unknown as User;
const createDto: CreateCorrespondenceDto = {
projectId: 'project-uuid',
typeId: 1,
subject: 'Test Subject',
recipients: [{ organizationId: 'recipient-uuid', type: 'TO' }],
};
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
const uuidResolver =
testingModule.get<UuidResolverService>(UuidResolverService);
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(100);
(uuidResolver.resolveOrganizationId as jest.Mock).mockResolvedValue(20);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
typeCode: 'LETTER',
});
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
statusCode: 'DRAFT',
});
(numberingService.generateNextNumber as jest.Mock).mockResolvedValue({
number: 'DOC-001',
});
mockDataSource.manager.findOne
.mockResolvedValueOnce({ id: 10, organizationCode: 'ORG' })
.mockResolvedValueOnce({ id: 20, organizationCode: 'REC' });
const queryRunner = {
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(
(_entity: unknown, payload: Record<string, unknown>) => payload
),
save: jest
.fn()
.mockResolvedValueOnce({ id: 999, publicId: 'corr-uuid' })
.mockResolvedValueOnce({ id: 1000 })
.mockResolvedValueOnce([]),
findOne: jest.fn(),
},
};
(mockDataSource.createQueryRunner as jest.Mock).mockReturnValue(
queryRunner
);
await service.create(createDto, mockUser);
expect(queryRunner.manager.create).toHaveBeenCalledWith(
CorrespondenceRevision,
expect.objectContaining({ revisionLabel: undefined })
);
});
it('should set revisionLabel to undefined for MEMO type', async () => {
const mockUser = {
user_id: 1,
primaryOrganizationId: 10,
} as unknown as User;
const createDto: CreateCorrespondenceDto = {
projectId: 'project-uuid',
typeId: 1,
subject: 'Test Subject',
recipients: [{ organizationId: 'recipient-uuid', type: 'TO' }],
};
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
const uuidResolver =
testingModule.get<UuidResolverService>(UuidResolverService);
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(100);
(uuidResolver.resolveOrganizationId as jest.Mock).mockResolvedValue(20);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
typeCode: 'MEMO',
});
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
statusCode: 'DRAFT',
});
(numberingService.generateNextNumber as jest.Mock).mockResolvedValue({
number: 'DOC-001',
});
mockDataSource.manager.findOne
.mockResolvedValueOnce({ id: 10, organizationCode: 'ORG' })
.mockResolvedValueOnce({ id: 20, organizationCode: 'REC' });
const queryRunner = {
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(
(_entity: unknown, payload: Record<string, unknown>) => payload
),
save: jest
.fn()
.mockResolvedValueOnce({ id: 999, publicId: 'corr-uuid' })
.mockResolvedValueOnce({ id: 1000 })
.mockResolvedValueOnce([]),
findOne: jest.fn(),
},
};
(mockDataSource.createQueryRunner as jest.Mock).mockReturnValue(
queryRunner
);
await service.create(createDto, mockUser);
expect(queryRunner.manager.create).toHaveBeenCalledWith(
CorrespondenceRevision,
expect.objectContaining({ revisionLabel: undefined })
);
});
});
});