/** * issue-ooc-resolve Controller Unit Tests * OOC Resolve Activity 발행 컨트롤러 테스트 */ import { TestDatabase } from '../../../../../test/test-database' import { withTestTransaction } from '../../../../../test/test-context' import { domainFactory, userFactory, roleFactory, dataSetFactory, dataOocFactory, activityFactory, activityInstanceFactory } from '../../../../../test/factories' import { DataOocStatus, ActivityInstanceStatus } from '../../../../../test/entities/schemas' describe('issue-ooc-resolve Controller', () => { let testDb: TestDatabase beforeAll(async () => { testDb = TestDatabase.getInstance() }) describe('OOC Resolve Activity 발행 조건', () => { it('Activity가 존재하고 resolverRole이 있으면 Resolve Instance가 생성되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given: OOC Resolve Activity와 resolverRole이 있는 DataSet const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx) const dataSet = await dataSetFactory.createWithDomain( { resolverRole }, domain, tx ) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED, correctiveInstruction: 'Temperature 조절 필요' }, dataSet, undefined, tx ) // When: issueOocResolve 로직 시뮬레이션 const activityInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 조치] ${dataSet.name}`, description: dataSet.description, state: ActivityInstanceStatus.Issued, dueAt: new Date(Date.now() + resolveActivity.standardTime * 1000), input: { dataOocId: dataOoc.id, instruction: dataOoc.correctiveInstruction }, assigneeRole: resolverRole, threadsMin: 1, threadsMax: 1, approvalLine: dataSet.outlierApprovalLine || [] }, resolveActivity, domain, tx ) // Then expect(activityInstance).toBeDefined() expect(activityInstance.name).toBe(`[OOC 조치] ${dataSet.name}`) expect(activityInstance.input?.dataOocId).toBe(dataOoc.id) expect(activityInstance.input?.instruction).toBe('Temperature 조절 필요') expect(activityInstance.assigneeRole?.id).toBe(resolverRole.id) expect(activityInstance.activity?.id).toBe(resolveActivity.id) }) }) it('resolverRole이 없으면 Resolve Activity가 발행되지 않아야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given: resolverRole이 없는 DataSet const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const dataSet = await dataSetFactory.createWithDomain( { resolverRole: undefined, resolverRoleId: undefined }, domain, tx ) // When & Then: resolverRole 체크 const resolverRoleId = dataSet.resolverRoleId expect(resolverRoleId).toBeFalsy() // issueOocResolve 로직에서는 이 경우 console.error만 출력하고 리턴 }) }) it('OOC Resolve Activity가 설치되지 않으면 Resolve가 발행되지 않아야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given: OOC Resolve Activity가 없는 상태 const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx) // When: Activity 조회 const activity = await tx .getRepository('Activity') .findOne({ where: { domain: { id: domain.id }, name: 'OOC Resolve' } }) // Then: Activity가 없음 expect(activity).toBeNull() // issueOocResolve 로직에서는 이 경우 console.error만 출력하고 리턴 }) }) }) describe('OOC Resolve Instance 속성', () => { it('input에 dataOocId와 instruction이 포함되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx) const dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED, correctiveInstruction: 'Temperature를 20-80 범위로 조절하세요' }, dataSet, undefined, tx ) // When: issueOocResolve 입력 구성 const input = { dataOocId: dataOoc.id, instruction: dataOoc.correctiveInstruction } // Then expect(input.dataOocId).toBe(dataOoc.id) expect(input.instruction).toBe('Temperature를 20-80 범위로 조절하세요') }) }) it('dueAt은 현재시간 + standardTime으로 계산되어야 한다 (Review와 다름)', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given const standardTime = 72 * 60 * 60 // 72시간 const resolveActivity = await activityFactory.createWithDomain( { name: 'OOC Resolve', standardTime }, domain, tx ) // When: dueAt 계산 (현재 시간 기준) const now = Date.now() const expectedDueAt = new Date(now + standardTime * 1000) // Then: dueAt이 현재시간 + standardTime // Review는 collectedAt 기준, Resolve는 현재시간 기준 expect(expectedDueAt.getTime()).toBeGreaterThan(now) expect(expectedDueAt.getTime() - now).toBe(standardTime * 1000) }) }) }) describe('outlierApprovalLine 적용', () => { it('DataSet의 outlierApprovalLine이 Resolve Instance에 적용되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given: outlierApprovalLine이 설정된 DataSet const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx) const outlierApprovalLine = [ { type: 'Role', value: approverRole.id, approver: { id: approverRole.id, name: approverRole.name } } ] const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx) const dataSet = await dataSetFactory.createWithDomain( { resolverRole, outlierApprovalLine }, domain, tx ) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) // When: Resolve Instance 생성 const activityInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued, assigneeRole: resolverRole, approvalLine: dataSet.outlierApprovalLine }, resolveActivity, domain, tx ) // Then expect(activityInstance.approvalLine).toBeDefined() expect(activityInstance.approvalLine?.length).toBe(1) expect(activityInstance.approvalLine?.[0].type).toBe('Role') expect(activityInstance.approvalLine?.[0].approver?.id).toBe(approverRole.id) }) }) it('outlierApprovalLine이 없으면 빈 배열 또는 undefined로 설정되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given: outlierApprovalLine이 없는 DataSet const { dataSet, resolverRole } = await dataSetFactory.createWithRoles( { outlierApprovalLine: undefined }, domain, tx ) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) // When: Resolve Instance 생성 const activityInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued, assigneeRole: resolverRole, approvalLine: dataSet.outlierApprovalLine || [] }, resolveActivity, domain, tx ) // Then expect(activityInstance.approvalLine).toEqual([]) }) }) it('다중 레벨 결재라인이 올바르게 적용되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given: 2단계 결재라인 const approverRole1 = await roleFactory.create({ name: 'Team Lead', domain }, tx) const approverRole2 = await roleFactory.create({ name: 'Manager', domain }, tx) const outlierApprovalLine = [ { type: 'Role', value: approverRole1.id, approver: { id: approverRole1.id, name: approverRole1.name } }, { type: 'Role', value: approverRole2.id, approver: { id: approverRole2.id, name: approverRole2.name } } ] const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx) const dataSet = await dataSetFactory.createWithDomain( { resolverRole, outlierApprovalLine }, domain, tx ) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) // When const activityInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued, assigneeRole: resolverRole, approvalLine: dataSet.outlierApprovalLine }, resolveActivity, domain, tx ) // Then expect(activityInstance.approvalLine?.length).toBe(2) expect(activityInstance.approvalLine?.[0].approver?.id).toBe(approverRole1.id) expect(activityInstance.approvalLine?.[1].approver?.id).toBe(approverRole2.id) }) }) it('Employee 타입 결재라인도 지원되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given: Employee 타입 결재라인 const approverUser = await userFactory.create({ name: 'Approver User' }, tx) const outlierApprovalLine = [ { type: 'Employee', value: approverUser.id, approver: { id: approverUser.id, name: approverUser.name } } ] const resolverRole = await roleFactory.create({ name: 'Resolver', domain }, tx) const dataSet = await dataSetFactory.createWithDomain( { resolverRole, outlierApprovalLine }, domain, tx ) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) // When const activityInstance = await activityInstanceFactory.createWithActivity( { state: ActivityInstanceStatus.Issued, assigneeRole: resolverRole, approvalLine: dataSet.outlierApprovalLine }, resolveActivity, domain, tx ) // Then expect(activityInstance.approvalLine?.[0].type).toBe('Employee') expect(activityInstance.approvalLine?.[0].approver?.id).toBe(approverUser.id) }) }) }) describe('DataOoc과 ResolveActivityInstance 연결', () => { it('DataOoc의 resolveActivityInstance 필드에 생성된 Instance가 저장되어야 한다', async () => { await withTestTransaction(async (context) => { const { tx, domain } = context.state // Given const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const { dataSet, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.REVIEWED, correctiveInstruction: 'Fix temperature' }, dataSet, undefined, tx ) // When: Resolve Instance 생성 및 DataOoc에 연결 const resolveInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 조치] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, input: { dataOocId: dataOoc.id, instruction: dataOoc.correctiveInstruction }, assigneeRole: resolverRole }, resolveActivity, domain, tx ) dataOoc.resolveActivityInstance = resolveInstance dataOoc = await tx.save('DataOoc', dataOoc) // Then expect(dataOoc.resolveActivityInstance).toBeDefined() expect(dataOoc.resolveActivityInstance?.id).toBe(resolveInstance.id) }) }) }) describe('Review → Resolve 연계', () => { it('Review 완료 후 Resolve가 자동으로 발행되는 플로우 테스트', async () => { await withTestTransaction(async (context) => { const { tx, domain, user } = context.state // Given: Review 완료 상태의 DataOoc const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx) const resolveActivity = await activityFactory.createOocResolveActivity(domain, tx) const { dataSet, supervisoryRole, resolverRole } = await dataSetFactory.createWithRoles({}, domain, tx) let dataOoc = await dataOocFactory.createWithDataSetAndSample( { state: DataOocStatus.ISSUED }, dataSet, undefined, tx ) // Review 완료 시뮬레이션 dataOoc.state = DataOocStatus.REVIEWED dataOoc.reviewedAt = new Date() dataOoc.reviewer = user dataOoc.correctiveInstruction = 'Temperature 조절 필요' dataOoc = await tx.save('DataOoc', dataOoc) // When: Resolve Activity 발행 (activity-ooc-review callback에서 호출됨) const resolveInstance = await activityInstanceFactory.createWithActivity( { name: `[OOC 조치] ${dataSet.name}`, state: ActivityInstanceStatus.Issued, input: { dataOocId: dataOoc.id, instruction: dataOoc.correctiveInstruction }, assigneeRole: resolverRole, approvalLine: dataSet.outlierApprovalLine || [] }, resolveActivity, domain, tx ) dataOoc.resolveActivityInstance = resolveInstance dataOoc = await tx.save('DataOoc', dataOoc) // Then: Review 완료 후 Resolve가 발행됨 expect(dataOoc.state).toBe(DataOocStatus.REVIEWED) expect(dataOoc.resolveActivityInstance).toBeDefined() expect(dataOoc.resolveActivityInstance?.input?.instruction).toBe('Temperature 조절 필요') }) }) }) })