@things-factory/dataset 9.1.19 → 9.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/activities/activity-data-review.js +5 -2
- package/dist-server/activities/activity-data-review.js.map +1 -1
- package/dist-server/activities/activity-ooc-review.js +4 -1
- package/dist-server/activities/activity-ooc-review.js.map +1 -1
- package/dist-server/controllers/create-data-ooc.js +2 -1
- package/dist-server/controllers/create-data-ooc.js.map +1 -1
- package/dist-server/controllers/create-data-sample.js +2 -2
- package/dist-server/controllers/create-data-sample.js.map +1 -1
- package/dist-server/controllers/issue-data-collection-task.js +9 -5
- package/dist-server/controllers/issue-data-collection-task.js.map +1 -1
- package/dist-server/controllers/issue-ooc-resolve.js +11 -6
- package/dist-server/controllers/issue-ooc-resolve.js.map +1 -1
- package/dist-server/controllers/issue-ooc-review.js +9 -6
- package/dist-server/controllers/issue-ooc-review.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/spec/integration/debug.spec.ts +42 -0
- package/spec/integration/ooc-lifecycle.spec.ts +484 -0
- package/spec/integration/ooc-workflow.spec.ts +276 -0
- package/spec/integration/simple.spec.ts +62 -0
- package/spec/unit/controllers/activity-callbacks.spec.ts +609 -0
- package/spec/unit/controllers/create-data-ooc.spec.ts +310 -0
- package/spec/unit/controllers/issue-ooc-resolve.spec.ts +431 -0
- package/spec/unit/controllers/issue-ooc-review.spec.ts +288 -0
- package/spec/unit/data-use-case.spec.ts +150 -0
- package/spec/unit/ooc-state-transition.spec.ts +233 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue-ooc-review Controller Unit Tests
|
|
3
|
+
* OOC Review Activity 발행 컨트롤러 테스트
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TestDatabase } from '../../../../../test/test-database'
|
|
7
|
+
import { withTestTransaction } from '../../../../../test/test-context'
|
|
8
|
+
import {
|
|
9
|
+
domainFactory,
|
|
10
|
+
userFactory,
|
|
11
|
+
roleFactory,
|
|
12
|
+
dataSetFactory,
|
|
13
|
+
dataSampleFactory,
|
|
14
|
+
dataOocFactory,
|
|
15
|
+
activityFactory,
|
|
16
|
+
activityInstanceFactory
|
|
17
|
+
} from '../../../../../test/factories'
|
|
18
|
+
import { DataOocStatus, ActivityInstanceStatus } from '../../../../../test/entities/schemas'
|
|
19
|
+
|
|
20
|
+
describe('issue-ooc-review Controller', () => {
|
|
21
|
+
let testDb: TestDatabase
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
testDb = TestDatabase.getInstance()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('OOC Review Activity 발행 조건', () => {
|
|
28
|
+
it('Activity가 존재하고 supervisoryRole이 있으면 Review Instance가 생성되어야 한다', async () => {
|
|
29
|
+
await withTestTransaction(async (context) => {
|
|
30
|
+
const { tx, domain } = context.state
|
|
31
|
+
|
|
32
|
+
// Given: OOC Review Activity와 supervisoryRole이 있는 DataSet
|
|
33
|
+
const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
|
|
34
|
+
const supervisoryRole = await roleFactory.create({ name: 'Supervisor', domain }, tx)
|
|
35
|
+
const dataSet = await dataSetFactory.createWithDomain(
|
|
36
|
+
{ supervisoryRole },
|
|
37
|
+
domain,
|
|
38
|
+
tx
|
|
39
|
+
)
|
|
40
|
+
const dataOoc = await dataOocFactory.createWithDataSetAndSample(
|
|
41
|
+
{ state: DataOocStatus.ISSUED },
|
|
42
|
+
dataSet,
|
|
43
|
+
undefined,
|
|
44
|
+
tx
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// When: issueOocReview 로직 시뮬레이션
|
|
48
|
+
const activityInstance = await activityInstanceFactory.createWithActivity(
|
|
49
|
+
{
|
|
50
|
+
name: `[OOC 검토] ${dataSet.name}`,
|
|
51
|
+
description: dataSet.description,
|
|
52
|
+
state: ActivityInstanceStatus.Issued,
|
|
53
|
+
dueAt: new Date(dataOoc.collectedAt.getTime() + reviewActivity.standardTime * 1000),
|
|
54
|
+
input: { dataOocId: dataOoc.id },
|
|
55
|
+
assigneeRole: supervisoryRole,
|
|
56
|
+
threadsMin: 1,
|
|
57
|
+
threadsMax: 1,
|
|
58
|
+
approvalLine: []
|
|
59
|
+
},
|
|
60
|
+
reviewActivity,
|
|
61
|
+
domain,
|
|
62
|
+
tx
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Then
|
|
66
|
+
expect(activityInstance).toBeDefined()
|
|
67
|
+
expect(activityInstance.name).toBe(`[OOC 검토] ${dataSet.name}`)
|
|
68
|
+
expect(activityInstance.input?.dataOocId).toBe(dataOoc.id)
|
|
69
|
+
expect(activityInstance.assigneeRole?.id).toBe(supervisoryRole.id)
|
|
70
|
+
expect(activityInstance.activity?.id).toBe(reviewActivity.id)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('supervisoryRole이 없으면 Review Activity가 발행되지 않아야 한다', async () => {
|
|
75
|
+
await withTestTransaction(async (context) => {
|
|
76
|
+
const { tx, domain } = context.state
|
|
77
|
+
|
|
78
|
+
// Given: supervisoryRole이 없는 DataSet
|
|
79
|
+
const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
|
|
80
|
+
const dataSet = await dataSetFactory.createWithDomain(
|
|
81
|
+
{ supervisoryRole: undefined, supervisoryRoleId: undefined },
|
|
82
|
+
domain,
|
|
83
|
+
tx
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// When & Then: supervisoryRole 체크
|
|
87
|
+
const supervisoryRoleId = dataSet.supervisoryRoleId
|
|
88
|
+
expect(supervisoryRoleId).toBeFalsy()
|
|
89
|
+
|
|
90
|
+
// issueOocReview 로직에서는 이 경우 console.error만 출력하고 리턴
|
|
91
|
+
// Activity Instance가 생성되지 않음
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('OOC Review Activity가 설치되지 않으면 Review가 발행되지 않아야 한다', async () => {
|
|
96
|
+
await withTestTransaction(async (context) => {
|
|
97
|
+
const { tx, domain } = context.state
|
|
98
|
+
|
|
99
|
+
// Given: OOC Review Activity가 없는 상태
|
|
100
|
+
const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
|
|
101
|
+
|
|
102
|
+
// When: Activity 조회
|
|
103
|
+
const activity = await tx
|
|
104
|
+
.getRepository('Activity')
|
|
105
|
+
.findOne({ where: { domain: { id: domain.id }, name: 'OOC Review' } })
|
|
106
|
+
|
|
107
|
+
// Then: Activity가 없음
|
|
108
|
+
expect(activity).toBeNull()
|
|
109
|
+
// issueOocReview 로직에서는 이 경우 console.warn만 출력하고 리턴
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('OOC Review Instance 속성', () => {
|
|
115
|
+
it('dueAt은 collectedAt + standardTime으로 계산되어야 한다', async () => {
|
|
116
|
+
await withTestTransaction(async (context) => {
|
|
117
|
+
const { tx, domain } = context.state
|
|
118
|
+
|
|
119
|
+
// Given
|
|
120
|
+
const standardTime = 48 * 60 * 60 // 48시간
|
|
121
|
+
const reviewActivity = await activityFactory.createWithDomain(
|
|
122
|
+
{
|
|
123
|
+
name: 'OOC Review',
|
|
124
|
+
standardTime
|
|
125
|
+
},
|
|
126
|
+
domain,
|
|
127
|
+
tx
|
|
128
|
+
)
|
|
129
|
+
const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
|
|
130
|
+
const collectedAt = new Date('2024-01-15T10:00:00.000Z')
|
|
131
|
+
const dataOoc = await dataOocFactory.createWithDataSetAndSample(
|
|
132
|
+
{ state: DataOocStatus.ISSUED, collectedAt },
|
|
133
|
+
dataSet,
|
|
134
|
+
undefined,
|
|
135
|
+
tx
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// When: dueAt 계산
|
|
139
|
+
const expectedDueAt = new Date(collectedAt.getTime() + standardTime * 1000)
|
|
140
|
+
|
|
141
|
+
// Then
|
|
142
|
+
expect(expectedDueAt.getTime()).toBe(collectedAt.getTime() + standardTime * 1000)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('input에 dataOocId가 포함되어야 한다', async () => {
|
|
147
|
+
await withTestTransaction(async (context) => {
|
|
148
|
+
const { tx, domain } = context.state
|
|
149
|
+
|
|
150
|
+
// Given
|
|
151
|
+
const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
|
|
152
|
+
const dataOoc = await dataOocFactory.createWithDataSetAndSample(
|
|
153
|
+
{ state: DataOocStatus.ISSUED },
|
|
154
|
+
dataSet,
|
|
155
|
+
undefined,
|
|
156
|
+
tx
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
// When: issueOocReview 입력 구성
|
|
160
|
+
const input = { dataOocId: dataOoc.id }
|
|
161
|
+
|
|
162
|
+
// Then
|
|
163
|
+
expect(input.dataOocId).toBe(dataOoc.id)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('threadsMin과 threadsMax가 1로 설정되어야 한다 (single 담당자)', async () => {
|
|
168
|
+
await withTestTransaction(async (context) => {
|
|
169
|
+
const { tx, domain } = context.state
|
|
170
|
+
|
|
171
|
+
// Given
|
|
172
|
+
const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
|
|
173
|
+
const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
|
|
174
|
+
|
|
175
|
+
// When: ActivityInstance 생성
|
|
176
|
+
const activityInstance = await activityInstanceFactory.createWithActivity(
|
|
177
|
+
{
|
|
178
|
+
threadsMin: 1,
|
|
179
|
+
threadsMax: 1,
|
|
180
|
+
assigneeRole: supervisoryRole
|
|
181
|
+
},
|
|
182
|
+
reviewActivity,
|
|
183
|
+
domain,
|
|
184
|
+
tx
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// Then
|
|
188
|
+
expect(activityInstance.threadsMin).toBe(1)
|
|
189
|
+
expect(activityInstance.threadsMax).toBe(1)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('approvalLine은 빈 배열이어야 한다 (Review는 결재 없음)', async () => {
|
|
194
|
+
await withTestTransaction(async (context) => {
|
|
195
|
+
const { tx, domain } = context.state
|
|
196
|
+
|
|
197
|
+
// Given
|
|
198
|
+
const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
|
|
199
|
+
const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
|
|
200
|
+
|
|
201
|
+
// When: ActivityInstance 생성 (issueOocReview 방식)
|
|
202
|
+
const activityInstance = await activityInstanceFactory.createWithActivity(
|
|
203
|
+
{
|
|
204
|
+
approvalLine: [],
|
|
205
|
+
assigneeRole: supervisoryRole
|
|
206
|
+
},
|
|
207
|
+
reviewActivity,
|
|
208
|
+
domain,
|
|
209
|
+
tx
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Then
|
|
213
|
+
expect(activityInstance.approvalLine).toEqual([])
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe('DataOoc과 ReviewActivityInstance 연결', () => {
|
|
219
|
+
it('DataOoc의 reviewActivityInstance 필드에 생성된 Instance가 저장되어야 한다', async () => {
|
|
220
|
+
await withTestTransaction(async (context) => {
|
|
221
|
+
const { tx, domain } = context.state
|
|
222
|
+
|
|
223
|
+
// Given
|
|
224
|
+
const reviewActivity = await activityFactory.createOocReviewActivity(domain, tx)
|
|
225
|
+
const { dataSet, supervisoryRole } = await dataSetFactory.createWithRoles({}, domain, tx)
|
|
226
|
+
let dataOoc = await dataOocFactory.createWithDataSetAndSample(
|
|
227
|
+
{ state: DataOocStatus.ISSUED },
|
|
228
|
+
dataSet,
|
|
229
|
+
undefined,
|
|
230
|
+
tx
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// When: Review Instance 생성 및 DataOoc에 연결
|
|
234
|
+
const reviewInstance = await activityInstanceFactory.createWithActivity(
|
|
235
|
+
{
|
|
236
|
+
name: `[OOC 검토] ${dataSet.name}`,
|
|
237
|
+
state: ActivityInstanceStatus.Issued,
|
|
238
|
+
input: { dataOocId: dataOoc.id },
|
|
239
|
+
assigneeRole: supervisoryRole
|
|
240
|
+
},
|
|
241
|
+
reviewActivity,
|
|
242
|
+
domain,
|
|
243
|
+
tx
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
dataOoc.reviewActivityInstance = reviewInstance
|
|
247
|
+
dataOoc = await tx.save('DataOoc', dataOoc)
|
|
248
|
+
|
|
249
|
+
// Then
|
|
250
|
+
expect(dataOoc.reviewActivityInstance).toBeDefined()
|
|
251
|
+
expect(dataOoc.reviewActivityInstance?.id).toBe(reviewInstance.id)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('Parent Domain 지원', () => {
|
|
257
|
+
it('domain.parentId가 있으면 parent domain의 Activity도 조회해야 한다', async () => {
|
|
258
|
+
await withTestTransaction(async (context) => {
|
|
259
|
+
const { tx } = context.state
|
|
260
|
+
|
|
261
|
+
// Given: Parent Domain과 Child Domain 구조
|
|
262
|
+
const parentDomain = await domainFactory.create({ name: 'Parent Domain' }, tx)
|
|
263
|
+
const childDomain = await domainFactory.create(
|
|
264
|
+
{
|
|
265
|
+
name: 'Child Domain',
|
|
266
|
+
parentId: parentDomain.id
|
|
267
|
+
},
|
|
268
|
+
tx
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// Parent Domain에 Activity 생성
|
|
272
|
+
const reviewActivity = await activityFactory.createOocReviewActivity(parentDomain, tx)
|
|
273
|
+
|
|
274
|
+
// When: Child Domain에서 Activity 조회 (In 연산자 사용)
|
|
275
|
+
const activities = await tx.getRepository('Activity').find({
|
|
276
|
+
where: [
|
|
277
|
+
{ domain: { id: childDomain.id }, name: 'OOC Review' },
|
|
278
|
+
{ domain: { id: parentDomain.id }, name: 'OOC Review' }
|
|
279
|
+
]
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// Then: Parent의 Activity가 조회됨
|
|
283
|
+
expect(activities.length).toBeGreaterThan(0)
|
|
284
|
+
expect(activities[0].id).toBe(reviewActivity.id)
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataUseCase Unit Tests
|
|
3
|
+
* 데이터 평가 로직 단위 테스트
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 단위 테스트에서는 dist 파일에서 직접 import하여 패키지 의존성 우회
|
|
7
|
+
import { DataUseCase } from '../../dist-server/controllers/data-use-case'
|
|
8
|
+
|
|
9
|
+
// 테스트용 타입 정의 (실제 타입과 호환)
|
|
10
|
+
interface DataItem {
|
|
11
|
+
name: string
|
|
12
|
+
tag?: string
|
|
13
|
+
active: boolean
|
|
14
|
+
type: string
|
|
15
|
+
spec?: Record<string, any>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface DataSet {
|
|
19
|
+
useCase: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('DataUseCase', () => {
|
|
23
|
+
describe('evaluate', () => {
|
|
24
|
+
it('useCase가 없으면 ooc/oos 모두 false를 반환해야 한다', () => {
|
|
25
|
+
// Given
|
|
26
|
+
const dataSet = { useCase: null } as DataSet
|
|
27
|
+
const dataItems: DataItem[] = []
|
|
28
|
+
const data = {}
|
|
29
|
+
|
|
30
|
+
// When
|
|
31
|
+
const result = DataUseCase.evaluate(dataSet, dataItems, data)
|
|
32
|
+
|
|
33
|
+
// Then
|
|
34
|
+
expect(result.ooc).toBe(false)
|
|
35
|
+
expect(result.oos).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('active가 false인 dataItem은 평가하지 않아야 한다', () => {
|
|
39
|
+
// Given
|
|
40
|
+
const dataSet = { useCase: 'QA' } as DataSet
|
|
41
|
+
const dataItems: DataItem[] = [
|
|
42
|
+
{
|
|
43
|
+
name: 'Temperature',
|
|
44
|
+
tag: 'temperature',
|
|
45
|
+
active: false,
|
|
46
|
+
type: 'number' as any,
|
|
47
|
+
spec: {
|
|
48
|
+
QA: { ucl: 80, lcl: 20 }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
const data = { temperature: 100 } // UCL 초과하지만 inactive
|
|
53
|
+
|
|
54
|
+
// When
|
|
55
|
+
const result = DataUseCase.evaluate(dataSet, dataItems, data)
|
|
56
|
+
|
|
57
|
+
// Then
|
|
58
|
+
expect(result.ooc).toBe(false)
|
|
59
|
+
expect(result.judgment).toEqual({})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('tag가 없는 dataItem은 평가하지 않아야 한다', () => {
|
|
63
|
+
// Given
|
|
64
|
+
const dataSet = { useCase: 'QA' } as DataSet
|
|
65
|
+
const dataItems: DataItem[] = [
|
|
66
|
+
{
|
|
67
|
+
name: 'Temperature',
|
|
68
|
+
tag: undefined,
|
|
69
|
+
active: true,
|
|
70
|
+
type: 'number' as any,
|
|
71
|
+
spec: {
|
|
72
|
+
QA: { ucl: 80, lcl: 20 }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
const data = { temperature: 100 }
|
|
77
|
+
|
|
78
|
+
// When
|
|
79
|
+
const result = DataUseCase.evaluate(dataSet, dataItems, data)
|
|
80
|
+
|
|
81
|
+
// Then
|
|
82
|
+
expect(result.ooc).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('값이 null이면 해당 항목을 건너뛰어야 한다', () => {
|
|
86
|
+
// Given
|
|
87
|
+
const dataSet = { useCase: 'QA' } as DataSet
|
|
88
|
+
const dataItems: DataItem[] = [
|
|
89
|
+
{
|
|
90
|
+
name: 'Temperature',
|
|
91
|
+
tag: 'temperature',
|
|
92
|
+
active: true,
|
|
93
|
+
type: 'number' as any,
|
|
94
|
+
spec: {
|
|
95
|
+
QA: { ucl: 80, lcl: 20 }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
const data = { temperature: null }
|
|
100
|
+
|
|
101
|
+
// When
|
|
102
|
+
const result = DataUseCase.evaluate(dataSet, dataItems, data)
|
|
103
|
+
|
|
104
|
+
// Then
|
|
105
|
+
expect(result.ooc).toBe(false)
|
|
106
|
+
expect(result.judgment).toEqual({})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('배열 형태의 값도 처리할 수 있어야 한다', () => {
|
|
110
|
+
// Given
|
|
111
|
+
const dataSet = { useCase: 'QA' } as DataSet
|
|
112
|
+
const dataItems: DataItem[] = [
|
|
113
|
+
{
|
|
114
|
+
name: 'Temperature',
|
|
115
|
+
tag: 'temperature',
|
|
116
|
+
active: true,
|
|
117
|
+
type: 'number' as any,
|
|
118
|
+
spec: {
|
|
119
|
+
QA: { ucl: 80, lcl: 20 }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
const data = { temperature: [50, 60, 70] } // 배열 형태
|
|
124
|
+
|
|
125
|
+
// When
|
|
126
|
+
const result = DataUseCase.evaluate(dataSet, dataItems, data)
|
|
127
|
+
|
|
128
|
+
// Then: 에러 없이 처리되어야 함
|
|
129
|
+
expect(result).toBeDefined()
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('registry', () => {
|
|
134
|
+
it('등록된 useCase 이름 목록을 반환해야 한다', () => {
|
|
135
|
+
// When
|
|
136
|
+
const names = DataUseCase.getUseCaseNames()
|
|
137
|
+
|
|
138
|
+
// Then
|
|
139
|
+
expect(Array.isArray(names)).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('존재하지 않는 useCase는 undefined를 반환해야 한다', () => {
|
|
143
|
+
// When
|
|
144
|
+
const useCase = DataUseCase.getUseCase('non-existent-use-case')
|
|
145
|
+
|
|
146
|
+
// Then
|
|
147
|
+
expect(useCase).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OOC State Transition Unit Tests
|
|
3
|
+
* DataOoc 상태 전이 로직 단위 테스트
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isValidOocState,
|
|
8
|
+
isValidOocTransition
|
|
9
|
+
} from '../../../../test/helpers/workflow-helpers'
|
|
10
|
+
import { DataOocStatus } from '../../../../test/entities/schemas'
|
|
11
|
+
|
|
12
|
+
describe('OOC State Transition Logic', () => {
|
|
13
|
+
describe('isValidOocState', () => {
|
|
14
|
+
it('ISSUED 상태는 유효해야 한다', () => {
|
|
15
|
+
const dataOoc = { state: DataOocStatus.ISSUED } as any
|
|
16
|
+
expect(isValidOocState(dataOoc)).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('REVIEWED 상태는 유효해야 한다', () => {
|
|
20
|
+
const dataOoc = { state: DataOocStatus.REVIEWED } as any
|
|
21
|
+
expect(isValidOocState(dataOoc)).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('CORRECTED 상태는 유효해야 한다', () => {
|
|
25
|
+
const dataOoc = { state: DataOocStatus.CORRECTED } as any
|
|
26
|
+
expect(isValidOocState(dataOoc)).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('REJECTED 상태는 유효해야 한다', () => {
|
|
30
|
+
const dataOoc = { state: DataOocStatus.REJECTED } as any
|
|
31
|
+
expect(isValidOocState(dataOoc)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('유효하지 않은 상태는 false를 반환해야 한다', () => {
|
|
35
|
+
const dataOoc = { state: 'INVALID_STATE' } as any
|
|
36
|
+
expect(isValidOocState(dataOoc)).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('isValidOocTransition', () => {
|
|
41
|
+
describe('유효한 상태 전이', () => {
|
|
42
|
+
it('ISSUED -> REVIEWED 전이는 유효해야 한다', () => {
|
|
43
|
+
expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.REVIEWED)).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('REVIEWED -> CORRECTED 전이는 유효해야 한다', () => {
|
|
47
|
+
expect(isValidOocTransition(DataOocStatus.REVIEWED, DataOocStatus.CORRECTED)).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('유효하지 않은 상태 전이', () => {
|
|
52
|
+
it('ISSUED -> CORRECTED 직접 전이는 불가능해야 한다', () => {
|
|
53
|
+
expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.CORRECTED)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('REVIEWED -> ISSUED 역방향 전이는 불가능해야 한다', () => {
|
|
57
|
+
expect(isValidOocTransition(DataOocStatus.REVIEWED, DataOocStatus.ISSUED)).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('CORRECTED -> REVIEWED 역방향 전이는 불가능해야 한다', () => {
|
|
61
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('CORRECTED -> ISSUED 역방향 전이는 불가능해야 한다', () => {
|
|
65
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('CORRECTED는 종료 상태이므로 더 이상 전이할 수 없다', () => {
|
|
69
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.CORRECTED)).toBe(false)
|
|
70
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false)
|
|
71
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('REJECTED는 종료 상태이므로 더 이상 전이할 수 없다', () => {
|
|
75
|
+
expect(isValidOocTransition(DataOocStatus.REJECTED, DataOocStatus.ISSUED)).toBe(false)
|
|
76
|
+
expect(isValidOocTransition(DataOocStatus.REJECTED, DataOocStatus.REVIEWED)).toBe(false)
|
|
77
|
+
expect(isValidOocTransition(DataOocStatus.REJECTED, DataOocStatus.CORRECTED)).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('같은 상태로의 전이는 불가능해야 한다', () => {
|
|
81
|
+
expect(isValidOocTransition(DataOocStatus.ISSUED, DataOocStatus.ISSUED)).toBe(false)
|
|
82
|
+
expect(isValidOocTransition(DataOocStatus.REVIEWED, DataOocStatus.REVIEWED)).toBe(false)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('DataOocStatus Enum', () => {
|
|
88
|
+
it('모든 상태 값이 정의되어야 한다', () => {
|
|
89
|
+
expect(DataOocStatus.ISSUED).toBe('ISSUED')
|
|
90
|
+
expect(DataOocStatus.REVIEWED).toBe('REVIEWED')
|
|
91
|
+
expect(DataOocStatus.CORRECTED).toBe('CORRECTED')
|
|
92
|
+
expect(DataOocStatus.REJECTED).toBe('REJECTED')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('총 4개의 상태가 있어야 한다', () => {
|
|
96
|
+
const statusValues = Object.values(DataOocStatus)
|
|
97
|
+
expect(statusValues.length).toBe(4)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('OOC State Rules', () => {
|
|
103
|
+
describe('ISSUED 상태 규칙', () => {
|
|
104
|
+
it('OOC 생성 시 초기 상태는 ISSUED여야 한다', () => {
|
|
105
|
+
// DataOoc이 생성될 때 기본 state는 ISSUED
|
|
106
|
+
expect(DataOocStatus.ISSUED).toBe('ISSUED')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('ISSUED 상태에서는 reviewedAt이 설정되지 않아야 한다', () => {
|
|
110
|
+
const dataOoc = {
|
|
111
|
+
state: DataOocStatus.ISSUED,
|
|
112
|
+
reviewedAt: null,
|
|
113
|
+
correctedAt: null
|
|
114
|
+
}
|
|
115
|
+
expect(dataOoc.reviewedAt).toBeNull()
|
|
116
|
+
expect(dataOoc.correctedAt).toBeNull()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('REVIEWED 상태 규칙', () => {
|
|
121
|
+
it('REVIEWED로 전이 시 reviewedAt이 설정되어야 한다', () => {
|
|
122
|
+
const now = new Date()
|
|
123
|
+
const dataOoc = {
|
|
124
|
+
state: DataOocStatus.REVIEWED,
|
|
125
|
+
reviewedAt: now,
|
|
126
|
+
correctedAt: null
|
|
127
|
+
}
|
|
128
|
+
expect(dataOoc.reviewedAt).toEqual(now)
|
|
129
|
+
expect(dataOoc.correctedAt).toBeNull()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('REVIEWED로 전이 시 reviewer가 설정되어야 한다', () => {
|
|
133
|
+
const reviewer = { id: 'user-1', name: 'Reviewer' }
|
|
134
|
+
const dataOoc = {
|
|
135
|
+
state: DataOocStatus.REVIEWED,
|
|
136
|
+
reviewer
|
|
137
|
+
}
|
|
138
|
+
expect(dataOoc.reviewer).toBeDefined()
|
|
139
|
+
expect(dataOoc.reviewer.id).toBe('user-1')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('REVIEWED로 전이 시 correctiveInstruction이 설정될 수 있다', () => {
|
|
143
|
+
const dataOoc = {
|
|
144
|
+
state: DataOocStatus.REVIEWED,
|
|
145
|
+
correctiveInstruction: 'Temperature를 조절하세요'
|
|
146
|
+
}
|
|
147
|
+
expect(dataOoc.correctiveInstruction).toBe('Temperature를 조절하세요')
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('CORRECTED 상태 규칙', () => {
|
|
152
|
+
it('CORRECTED로 전이 시 correctedAt이 설정되어야 한다', () => {
|
|
153
|
+
const now = new Date()
|
|
154
|
+
const dataOoc = {
|
|
155
|
+
state: DataOocStatus.CORRECTED,
|
|
156
|
+
reviewedAt: new Date(Date.now() - 3600000), // 1시간 전
|
|
157
|
+
correctedAt: now
|
|
158
|
+
}
|
|
159
|
+
expect(dataOoc.correctedAt).toEqual(now)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('CORRECTED로 전이 시 corrector가 설정되어야 한다', () => {
|
|
163
|
+
const corrector = { id: 'user-2', name: 'Corrector' }
|
|
164
|
+
const dataOoc = {
|
|
165
|
+
state: DataOocStatus.CORRECTED,
|
|
166
|
+
corrector
|
|
167
|
+
}
|
|
168
|
+
expect(dataOoc.corrector).toBeDefined()
|
|
169
|
+
expect(dataOoc.corrector.id).toBe('user-2')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('CORRECTED로 전이 시 correctiveAction이 설정되어야 한다', () => {
|
|
173
|
+
const dataOoc = {
|
|
174
|
+
state: DataOocStatus.CORRECTED,
|
|
175
|
+
correctiveAction: 'Temperature를 정상 범위로 조절함'
|
|
176
|
+
}
|
|
177
|
+
expect(dataOoc.correctiveAction).toBe('Temperature를 정상 범위로 조절함')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('CORRECTED는 종료 상태이다', () => {
|
|
181
|
+
// 더 이상 전이 불가
|
|
182
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.ISSUED)).toBe(false)
|
|
183
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.REVIEWED)).toBe(false)
|
|
184
|
+
expect(isValidOocTransition(DataOocStatus.CORRECTED, DataOocStatus.CORRECTED)).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('History 추적 규칙', () => {
|
|
189
|
+
it('상태 전이 시 history에 기록이 추가되어야 한다', () => {
|
|
190
|
+
const history = [
|
|
191
|
+
{
|
|
192
|
+
user: { id: 'user-1', name: 'Creator' },
|
|
193
|
+
state: DataOocStatus.ISSUED,
|
|
194
|
+
timestamp: '2024-01-01T00:00:00.000Z'
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
user: { id: 'user-2', name: 'Reviewer' },
|
|
198
|
+
state: DataOocStatus.REVIEWED,
|
|
199
|
+
timestamp: '2024-01-01T01:00:00.000Z'
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
expect(history.length).toBe(2)
|
|
204
|
+
expect(history[0].state).toBe(DataOocStatus.ISSUED)
|
|
205
|
+
expect(history[1].state).toBe(DataOocStatus.REVIEWED)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('history 엔트리에는 user, state, timestamp가 있어야 한다', () => {
|
|
209
|
+
const historyEntry = {
|
|
210
|
+
user: { id: 'user-1', name: 'User' },
|
|
211
|
+
state: DataOocStatus.ISSUED,
|
|
212
|
+
timestamp: new Date().toISOString()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(historyEntry.user).toBeDefined()
|
|
216
|
+
expect(historyEntry.user.id).toBeDefined()
|
|
217
|
+
expect(historyEntry.user.name).toBeDefined()
|
|
218
|
+
expect(historyEntry.state).toBeDefined()
|
|
219
|
+
expect(historyEntry.timestamp).toBeDefined()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('CORRECTED 전이 시 history에 comment(correctiveAction)가 포함될 수 있다', () => {
|
|
223
|
+
const historyEntry = {
|
|
224
|
+
user: { id: 'user-1', name: 'Corrector' },
|
|
225
|
+
state: DataOocStatus.CORRECTED,
|
|
226
|
+
comment: 'Temperature를 정상 범위로 조절함',
|
|
227
|
+
timestamp: new Date().toISOString()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
expect(historyEntry.comment).toBe('Temperature를 정상 범위로 조절함')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
})
|