@things-factory/worklist 9.1.19 → 10.0.0-beta.2
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/components/activity-thread-timeline.d.ts +1 -9
- package/dist-client/components/activity-thread-timeline.js +1 -3
- package/dist-client/components/activity-thread-timeline.js.map +1 -1
- package/dist-client/pages/activity/activity-list-page.d.ts +1 -7
- package/dist-client/pages/activity/activity-list-page.js +125 -234
- package/dist-client/pages/activity/activity-list-page.js.map +1 -1
- package/dist-client/pages/activity/activity-page.d.ts +1 -7
- package/dist-client/pages/activity/activity-page.js +51 -93
- package/dist-client/pages/activity/activity-page.js.map +1 -1
- package/dist-client/pages/activity/starter-list-page.d.ts +1 -7
- package/dist-client/pages/activity/starter-list-page.js +33 -62
- package/dist-client/pages/activity/starter-list-page.js.map +1 -1
- package/dist-client/pages/activity-approval/activity-approval-list-page.d.ts +1 -7
- package/dist-client/pages/activity-approval/activity-approval-list-page.js +50 -95
- package/dist-client/pages/activity-approval/activity-approval-list-page.js.map +1 -1
- package/dist-client/pages/activity-approval/activity-approval-page.d.ts +1 -7
- package/dist-client/pages/activity-approval/activity-approval-page.js +73 -119
- package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-list-page.d.ts +0 -6
- package/dist-client/pages/activity-instance/activity-instance-list-page.js +63 -120
- package/dist-client/pages/activity-instance/activity-instance-list-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-search-page.d.ts +1 -7
- package/dist-client/pages/activity-instance/activity-instance-search-page.js +55 -101
- package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-start-page.d.ts +1 -7
- package/dist-client/pages/activity-instance/activity-instance-start-page.js +65 -109
- package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
- package/dist-client/pages/activity-stats/activity-stats-list-page.d.ts +1 -7
- package/dist-client/pages/activity-stats/activity-stats-list-page.js +50 -95
- package/dist-client/pages/activity-stats/activity-stats-list-page.js.map +1 -1
- package/dist-client/pages/activity-store/activity-store-page.d.ts +1 -7
- package/dist-client/pages/activity-store/activity-store-page.js +2 -3
- package/dist-client/pages/activity-store/activity-store-page.js.map +1 -1
- package/dist-client/pages/activity-supervisor/reporter-list-page.d.ts +1 -7
- package/dist-client/pages/activity-supervisor/reporter-list-page.js +36 -66
- package/dist-client/pages/activity-supervisor/reporter-list-page.js.map +1 -1
- package/dist-client/pages/activity-template/activity-template-list-page.d.ts +1 -7
- package/dist-client/pages/activity-template/activity-template-list-page.js +70 -134
- package/dist-client/pages/activity-template/activity-template-list-page.js.map +1 -1
- package/dist-client/pages/activity-thread/activity-thread-list-page.d.ts +1 -7
- package/dist-client/pages/activity-thread/activity-thread-list-page.js +49 -93
- package/dist-client/pages/activity-thread/activity-thread-list-page.js.map +1 -1
- package/dist-client/pages/activity-thread/activity-thread-page.d.ts +1 -7
- package/dist-client/pages/activity-thread/activity-thread-page.js +80 -135
- package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
- package/dist-client/pages/activity-thread/activity-thread-view-page.d.ts +1 -7
- package/dist-client/pages/activity-thread/activity-thread-view-page.js +54 -80
- package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
- package/dist-client/pages/activity-thread/activity-thread-view.js +4 -0
- package/dist-client/pages/activity-thread/activity-thread-view.js.map +1 -1
- package/dist-client/pages/dashboard/dashboard-home.js +3 -5
- package/dist-client/pages/dashboard/dashboard-home.js.map +1 -1
- package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +0 -6
- package/dist-client/pages/installable-activity/installable-activity-list-page.js +68 -130
- package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
- package/dist-client/pages/todo/approval-done-list-page.d.ts +1 -7
- package/dist-client/pages/todo/approval-done-list-page.js +53 -100
- package/dist-client/pages/todo/approval-done-list-page.js.map +1 -1
- package/dist-client/pages/todo/approval-pending-list-page.d.ts +0 -6
- package/dist-client/pages/todo/approval-pending-list-page.js +63 -119
- package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
- package/dist-client/pages/todo/done-list-calendar-page.d.ts +1 -7
- package/dist-client/pages/todo/done-list-calendar-page.js +2 -3
- package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
- package/dist-client/pages/todo/done-list-page.d.ts +1 -7
- package/dist-client/pages/todo/done-list-page.js +56 -106
- package/dist-client/pages/todo/done-list-page.js.map +1 -1
- package/dist-client/pages/todo/draft-list-page.d.ts +1 -7
- package/dist-client/pages/todo/draft-list-page.js +49 -88
- package/dist-client/pages/todo/draft-list-page.js.map +1 -1
- package/dist-client/pages/todo/pickable-list-page.d.ts +1 -7
- package/dist-client/pages/todo/pickable-list-page.js +48 -91
- package/dist-client/pages/todo/pickable-list-page.js.map +1 -1
- package/dist-client/pages/todo/todo-list-page.d.ts +0 -6
- package/dist-client/pages/todo/todo-list-page.js +56 -106
- package/dist-client/pages/todo/todo-list-page.js.map +1 -1
- package/dist-client/pages/worklist-home.js +2 -3
- package/dist-client/pages/worklist-home.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/templates/activity-approval-context-template.js +8 -12
- package/dist-client/templates/activity-approval-context-template.js.map +1 -1
- package/dist-client/templates/activity-instance-context-template.js +8 -12
- package/dist-client/templates/activity-instance-context-template.js.map +1 -1
- package/dist-client/templates/activity-thread-context-template.js +8 -12
- package/dist-client/templates/activity-thread-context-template.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/activity-approval/approve.js +2 -2
- package/dist-server/controllers/activity-approval/approve.js.map +1 -1
- package/dist-server/controllers/activity-thread/submit.js +2 -2
- package/dist-server/controllers/activity-thread/submit.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -2
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/spec/integration/approval-mixed-types.spec.ts +491 -0
- package/spec/integration/approval-role-based.spec.ts +389 -0
- package/spec/integration/instance-lifecycle.spec.ts +406 -0
- package/spec/integration/role-approval-edge-cases.spec.ts +581 -0
- package/spec/unit/controllers/activity-instance-issue.spec.ts +360 -0
- package/spec/unit/controllers/activity-thread-submit.spec.ts +384 -0
- package/spec/unit/role-approval-escalate-logic.spec.ts +499 -0
- package/spec/unit/role-approval-submit-logic.spec.ts +481 -0
- package/spec/unit/thread-state-helpers.spec.ts +253 -0
- package/translations/en.json +1 -1
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ROLE Approval Escalate Logic Tests
|
|
3
|
+
* ROLE 타입 승인라인 에스컬레이션 로직 검증 테스트
|
|
4
|
+
*
|
|
5
|
+
* 수정된 버그:
|
|
6
|
+
* - approve.ts:66 - approvalLine[0].value → approvalLine[order].value
|
|
7
|
+
* - approve.ts:68 - if (approverRoleId) → if (!approverRoleId)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TestDatabase } from '../../../../test/test-database'
|
|
11
|
+
import { withTestTransaction } from '../../../../test/test-context'
|
|
12
|
+
import {
|
|
13
|
+
domainFactory,
|
|
14
|
+
userFactory,
|
|
15
|
+
roleFactory,
|
|
16
|
+
activityInstanceFactory,
|
|
17
|
+
activityThreadFactory,
|
|
18
|
+
activityApprovalFactory
|
|
19
|
+
} from '../../../../test/factories'
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
ActivityInstanceStatus,
|
|
23
|
+
ActivityThreadStatus,
|
|
24
|
+
ActivityApprovalJudgment
|
|
25
|
+
} from '../../../../test/entities/schemas'
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
createRoleApprovalLineItem,
|
|
29
|
+
createUserApprovalLineItem,
|
|
30
|
+
createMultiLevelApprovalLine
|
|
31
|
+
} from '../../../../test/helpers/workflow-helpers'
|
|
32
|
+
|
|
33
|
+
describe('ROLE Approval Escalate Logic Tests', () => {
|
|
34
|
+
let testDb: TestDatabase
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
testDb = TestDatabase.getInstance()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('단일 ROLE 승인 (에스컬레이션 없음)', () => {
|
|
41
|
+
it('단일 ROLE 승인라인에서 승인 시 judgment가 Approved가 되어야 한다', async () => {
|
|
42
|
+
await withTestTransaction(async (context) => {
|
|
43
|
+
const { tx } = context.state
|
|
44
|
+
const domain = await domainFactory.create({}, tx)
|
|
45
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
46
|
+
const approver = await userFactory.createWithRole({}, approverRole, tx)
|
|
47
|
+
|
|
48
|
+
// Given: 단일 ROLE 승인라인
|
|
49
|
+
const approvalLine = [createRoleApprovalLineItem(approverRole)]
|
|
50
|
+
const instance = await activityInstanceFactory.createWithApprovalLine(
|
|
51
|
+
approvalLine,
|
|
52
|
+
{ state: ActivityInstanceStatus.Started },
|
|
53
|
+
undefined,
|
|
54
|
+
domain,
|
|
55
|
+
tx
|
|
56
|
+
)
|
|
57
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
58
|
+
{ state: ActivityThreadStatus.Submitted },
|
|
59
|
+
instance,
|
|
60
|
+
approver,
|
|
61
|
+
tx
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// Given: order=1인 Pending approval
|
|
65
|
+
const approval = await tx.save('ActivityApproval', {
|
|
66
|
+
domain,
|
|
67
|
+
activityThread: thread,
|
|
68
|
+
round: 1,
|
|
69
|
+
order: 1,
|
|
70
|
+
judgment: ActivityApprovalJudgment.Pending,
|
|
71
|
+
approverRole: approverRole,
|
|
72
|
+
creator: approver,
|
|
73
|
+
updater: approver
|
|
74
|
+
}) as any
|
|
75
|
+
|
|
76
|
+
// When: 승인 (approvalLine[order]가 없으므로 최종 승인)
|
|
77
|
+
const order = approval.order // 1
|
|
78
|
+
const hasNextLevel = approvalLine.length > order // 1 > 1 = false
|
|
79
|
+
expect(hasNextLevel).toBe(false)
|
|
80
|
+
|
|
81
|
+
approval.judgment = ActivityApprovalJudgment.Approved
|
|
82
|
+
approval.approver = approver
|
|
83
|
+
approval.terminatedAt = new Date()
|
|
84
|
+
const approved = await tx.save('ActivityApproval', approval) as any
|
|
85
|
+
|
|
86
|
+
// Then
|
|
87
|
+
expect(approved.judgment).toBe(ActivityApprovalJudgment.Approved)
|
|
88
|
+
expect(approved.approver?.id).toBe(approver.id)
|
|
89
|
+
expect(approved.terminatedAt).toBeDefined()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('다단계 ROLE 에스컬레이션', () => {
|
|
95
|
+
it('2단계 ROLE 승인라인에서 첫 번째 승인 시 Escalated가 되어야 한다', async () => {
|
|
96
|
+
await withTestTransaction(async (context) => {
|
|
97
|
+
const { tx } = context.state
|
|
98
|
+
const domain = await domainFactory.create({}, tx)
|
|
99
|
+
const firstRole = await roleFactory.create({ name: 'First Approver', domain }, tx)
|
|
100
|
+
const secondRole = await roleFactory.create({ name: 'Second Approver', domain }, tx)
|
|
101
|
+
const firstApprover = await userFactory.createWithRole({}, firstRole, tx)
|
|
102
|
+
|
|
103
|
+
// Given: 2단계 ROLE 승인라인
|
|
104
|
+
const approvalLine = [
|
|
105
|
+
createRoleApprovalLineItem(firstRole),
|
|
106
|
+
createRoleApprovalLineItem(secondRole)
|
|
107
|
+
]
|
|
108
|
+
const instance = await activityInstanceFactory.createWithApprovalLine(
|
|
109
|
+
approvalLine,
|
|
110
|
+
{ state: ActivityInstanceStatus.Started },
|
|
111
|
+
undefined,
|
|
112
|
+
domain,
|
|
113
|
+
tx
|
|
114
|
+
)
|
|
115
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
116
|
+
{ state: ActivityThreadStatus.Submitted },
|
|
117
|
+
instance,
|
|
118
|
+
firstApprover,
|
|
119
|
+
tx
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// Given: order=1인 첫 번째 approval
|
|
123
|
+
const firstApproval = await tx.save('ActivityApproval', {
|
|
124
|
+
domain,
|
|
125
|
+
activityThread: thread,
|
|
126
|
+
round: 1,
|
|
127
|
+
order: 1,
|
|
128
|
+
judgment: ActivityApprovalJudgment.Pending,
|
|
129
|
+
approverRole: firstRole,
|
|
130
|
+
creator: firstApprover,
|
|
131
|
+
updater: firstApprover
|
|
132
|
+
}) as any
|
|
133
|
+
|
|
134
|
+
// When: 첫 번째 승인 (order=1, approvalLine[1] 존재)
|
|
135
|
+
const order = firstApproval.order // 1
|
|
136
|
+
const hasNextLevel = approvalLine.length > order // 2 > 1 = true
|
|
137
|
+
expect(hasNextLevel).toBe(true)
|
|
138
|
+
|
|
139
|
+
// Then: Escalated 상태가 됨
|
|
140
|
+
firstApproval.judgment = ActivityApprovalJudgment.Escalated
|
|
141
|
+
firstApproval.approver = firstApprover
|
|
142
|
+
firstApproval.terminatedAt = new Date()
|
|
143
|
+
const escalated = await tx.save('ActivityApproval', firstApproval) as any
|
|
144
|
+
|
|
145
|
+
expect(escalated.judgment).toBe(ActivityApprovalJudgment.Escalated)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('에스컬레이션 시 다음 레벨의 roleId를 올바르게 참조해야 한다 (버그 수정 검증)', async () => {
|
|
150
|
+
await withTestTransaction(async (context) => {
|
|
151
|
+
const { tx } = context.state
|
|
152
|
+
const domain = await domainFactory.create({}, tx)
|
|
153
|
+
const firstRole = await roleFactory.create({ name: 'First Role', domain }, tx)
|
|
154
|
+
const secondRole = await roleFactory.create({ name: 'Second Role', domain }, tx)
|
|
155
|
+
const thirdRole = await roleFactory.create({ name: 'Third Role', domain }, tx)
|
|
156
|
+
|
|
157
|
+
// Given: 3단계 ROLE 승인라인
|
|
158
|
+
const approvalLine = [
|
|
159
|
+
createRoleApprovalLineItem(firstRole),
|
|
160
|
+
createRoleApprovalLineItem(secondRole),
|
|
161
|
+
createRoleApprovalLineItem(thirdRole)
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
// 버그 수정 검증: approvalLine[order].value를 사용해야 함
|
|
165
|
+
// 수정 전: approvalLine[0].value - 항상 첫 번째 역할만 참조
|
|
166
|
+
// 수정 후: approvalLine[order].value - 올바른 인덱스 참조
|
|
167
|
+
|
|
168
|
+
// order=1일 때 (첫 번째 approval 완료 후)
|
|
169
|
+
const order1 = 1
|
|
170
|
+
const nextRoleId1 = approvalLine[order1].value
|
|
171
|
+
expect(nextRoleId1).toBe(secondRole.id)
|
|
172
|
+
expect(nextRoleId1).not.toBe(firstRole.id) // 수정 전 버그는 firstRole.id 반환
|
|
173
|
+
|
|
174
|
+
// order=2일 때 (두 번째 approval 완료 후)
|
|
175
|
+
const order2 = 2
|
|
176
|
+
const nextRoleId2 = approvalLine[order2].value
|
|
177
|
+
expect(nextRoleId2).toBe(thirdRole.id)
|
|
178
|
+
expect(nextRoleId2).not.toBe(firstRole.id) // 수정 전 버그는 firstRole.id 반환
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('3단계 ROLE 승인라인에서 각 레벨의 roleId가 올바르게 추출되어야 한다', async () => {
|
|
183
|
+
await withTestTransaction(async (context) => {
|
|
184
|
+
const { tx } = context.state
|
|
185
|
+
const domain = await domainFactory.create({}, tx)
|
|
186
|
+
const role1 = await roleFactory.create({ name: 'Level 1', domain }, tx)
|
|
187
|
+
const role2 = await roleFactory.create({ name: 'Level 2', domain }, tx)
|
|
188
|
+
const role3 = await roleFactory.create({ name: 'Level 3', domain }, tx)
|
|
189
|
+
|
|
190
|
+
const approvalLine = [
|
|
191
|
+
createRoleApprovalLineItem(role1),
|
|
192
|
+
createRoleApprovalLineItem(role2),
|
|
193
|
+
createRoleApprovalLineItem(role3)
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
// order=0 (첫 번째 승인자 할당 시)
|
|
197
|
+
expect(approvalLine[0].value).toBe(role1.id)
|
|
198
|
+
expect(approvalLine[0].type).toBe('Role')
|
|
199
|
+
|
|
200
|
+
// order=1 (첫 번째 에스컬레이션 시)
|
|
201
|
+
expect(approvalLine[1].value).toBe(role2.id)
|
|
202
|
+
expect(approvalLine[1].type).toBe('Role')
|
|
203
|
+
|
|
204
|
+
// order=2 (두 번째 에스컬레이션 시)
|
|
205
|
+
expect(approvalLine[2].value).toBe(role3.id)
|
|
206
|
+
expect(approvalLine[2].type).toBe('Role')
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('에스컬레이션 시 다음 레벨 ActivityApproval이 생성되어야 한다', async () => {
|
|
211
|
+
await withTestTransaction(async (context) => {
|
|
212
|
+
const { tx } = context.state
|
|
213
|
+
const domain = await domainFactory.create({}, tx)
|
|
214
|
+
const firstRole = await roleFactory.create({ name: 'First', domain }, tx)
|
|
215
|
+
const secondRole = await roleFactory.create({ name: 'Second', domain }, tx)
|
|
216
|
+
const firstApprover = await userFactory.createWithRole({}, firstRole, tx)
|
|
217
|
+
|
|
218
|
+
// Given: 2단계 승인라인
|
|
219
|
+
const approvalLine = [
|
|
220
|
+
createRoleApprovalLineItem(firstRole),
|
|
221
|
+
createRoleApprovalLineItem(secondRole)
|
|
222
|
+
]
|
|
223
|
+
const instance = await activityInstanceFactory.createWithApprovalLine(
|
|
224
|
+
approvalLine,
|
|
225
|
+
{ state: ActivityInstanceStatus.Started },
|
|
226
|
+
undefined,
|
|
227
|
+
domain,
|
|
228
|
+
tx
|
|
229
|
+
)
|
|
230
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
231
|
+
{ state: ActivityThreadStatus.Submitted },
|
|
232
|
+
instance,
|
|
233
|
+
firstApprover,
|
|
234
|
+
tx
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Given: 첫 번째 approval 완료
|
|
238
|
+
const firstApproval = await tx.save('ActivityApproval', {
|
|
239
|
+
domain,
|
|
240
|
+
activityThread: thread,
|
|
241
|
+
round: 1,
|
|
242
|
+
order: 1,
|
|
243
|
+
judgment: ActivityApprovalJudgment.Escalated,
|
|
244
|
+
approverRole: firstRole,
|
|
245
|
+
approver: firstApprover,
|
|
246
|
+
terminatedAt: new Date(),
|
|
247
|
+
creator: firstApprover,
|
|
248
|
+
updater: firstApprover
|
|
249
|
+
}) as any
|
|
250
|
+
|
|
251
|
+
// When: 다음 레벨 approval 생성
|
|
252
|
+
const order = firstApproval.order // 1
|
|
253
|
+
const nextApproverRoleId = approvalLine[order].value // approvalLine[1].value = secondRole.id
|
|
254
|
+
expect(nextApproverRoleId).toBe(secondRole.id)
|
|
255
|
+
|
|
256
|
+
const secondApproval = await tx.save('ActivityApproval', {
|
|
257
|
+
domain,
|
|
258
|
+
activityThread: thread,
|
|
259
|
+
round: 1,
|
|
260
|
+
order: order + 1, // 2
|
|
261
|
+
judgment: ActivityApprovalJudgment.Pending,
|
|
262
|
+
approverRole: secondRole,
|
|
263
|
+
transaction: 'escalate',
|
|
264
|
+
creator: firstApprover,
|
|
265
|
+
updater: firstApprover
|
|
266
|
+
}) as any
|
|
267
|
+
|
|
268
|
+
// Then
|
|
269
|
+
expect(secondApproval.order).toBe(2)
|
|
270
|
+
expect(secondApproval.approverRole?.id).toBe(secondRole.id)
|
|
271
|
+
expect(secondApproval.approver).toBeUndefined()
|
|
272
|
+
expect(secondApproval.judgment).toBe(ActivityApprovalJudgment.Pending)
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('에스컬레이션 후 스레드 상태', () => {
|
|
278
|
+
it('에스컬레이션 시 스레드 상태가 Escalated가 되어야 한다', async () => {
|
|
279
|
+
await withTestTransaction(async (context) => {
|
|
280
|
+
const { tx } = context.state
|
|
281
|
+
const domain = await domainFactory.create({}, tx)
|
|
282
|
+
const user = await userFactory.create({}, tx)
|
|
283
|
+
|
|
284
|
+
// Given: Submitted 상태 스레드
|
|
285
|
+
const instance = await activityInstanceFactory.createWithActivity(
|
|
286
|
+
{ state: ActivityInstanceStatus.Started },
|
|
287
|
+
undefined,
|
|
288
|
+
domain,
|
|
289
|
+
tx
|
|
290
|
+
)
|
|
291
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
292
|
+
{ state: ActivityThreadStatus.Submitted },
|
|
293
|
+
instance,
|
|
294
|
+
user,
|
|
295
|
+
tx
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// When: 에스컬레이션
|
|
299
|
+
thread.state = ActivityThreadStatus.Escalated
|
|
300
|
+
const escalated = await tx.save('ActivityThread', thread) as any
|
|
301
|
+
|
|
302
|
+
// Then
|
|
303
|
+
expect(escalated.state).toBe(ActivityThreadStatus.Escalated)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('최종 승인 시 스레드 상태가 Ended가 되어야 한다', async () => {
|
|
308
|
+
await withTestTransaction(async (context) => {
|
|
309
|
+
const { tx } = context.state
|
|
310
|
+
const domain = await domainFactory.create({}, tx)
|
|
311
|
+
const user = await userFactory.create({}, tx)
|
|
312
|
+
|
|
313
|
+
// Given: Escalated 상태 스레드
|
|
314
|
+
const instance = await activityInstanceFactory.createWithActivity(
|
|
315
|
+
{ state: ActivityInstanceStatus.Started },
|
|
316
|
+
undefined,
|
|
317
|
+
domain,
|
|
318
|
+
tx
|
|
319
|
+
)
|
|
320
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
321
|
+
{ state: ActivityThreadStatus.Escalated },
|
|
322
|
+
instance,
|
|
323
|
+
user,
|
|
324
|
+
tx
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
// When: 최종 승인
|
|
328
|
+
thread.state = ActivityThreadStatus.Ended
|
|
329
|
+
thread.terminatedAt = new Date()
|
|
330
|
+
const ended = await tx.save('ActivityThread', thread) as any
|
|
331
|
+
|
|
332
|
+
// Then
|
|
333
|
+
expect(ended.state).toBe(ActivityThreadStatus.Ended)
|
|
334
|
+
expect(ended.terminatedAt).toBeDefined()
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('승인 전 조건 검증', () => {
|
|
340
|
+
it('Pending 상태의 approval만 승인/반려 가능해야 한다', async () => {
|
|
341
|
+
await withTestTransaction(async (context) => {
|
|
342
|
+
const { tx } = context.state
|
|
343
|
+
const domain = await domainFactory.create({}, tx)
|
|
344
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
345
|
+
|
|
346
|
+
// Pending 상태: 승인 가능
|
|
347
|
+
const pendingApproval = await activityApprovalFactory.createWithRole(
|
|
348
|
+
{ judgment: ActivityApprovalJudgment.Pending },
|
|
349
|
+
undefined,
|
|
350
|
+
approverRole,
|
|
351
|
+
tx
|
|
352
|
+
)
|
|
353
|
+
expect(pendingApproval.judgment).toBe(ActivityApprovalJudgment.Pending)
|
|
354
|
+
const canApprovePending = pendingApproval.judgment === ActivityApprovalJudgment.Pending
|
|
355
|
+
expect(canApprovePending).toBe(true)
|
|
356
|
+
|
|
357
|
+
// Approved 상태: 승인 불가
|
|
358
|
+
const approvedApproval = await activityApprovalFactory.createWithRole(
|
|
359
|
+
{ judgment: ActivityApprovalJudgment.Approved },
|
|
360
|
+
undefined,
|
|
361
|
+
approverRole,
|
|
362
|
+
tx
|
|
363
|
+
)
|
|
364
|
+
const canApproveApproved = approvedApproval.judgment === ActivityApprovalJudgment.Pending
|
|
365
|
+
expect(canApproveApproved).toBe(false)
|
|
366
|
+
|
|
367
|
+
// Rejected 상태: 승인 불가
|
|
368
|
+
const rejectedApproval = await activityApprovalFactory.createWithRole(
|
|
369
|
+
{ judgment: ActivityApprovalJudgment.Rejected },
|
|
370
|
+
undefined,
|
|
371
|
+
approverRole,
|
|
372
|
+
tx
|
|
373
|
+
)
|
|
374
|
+
const canApproveRejected = rejectedApproval.judgment === ActivityApprovalJudgment.Pending
|
|
375
|
+
expect(canApproveRejected).toBe(false)
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
describe('ROLE approval 권한 검증', () => {
|
|
381
|
+
it('해당 Role을 가진 사용자만 승인할 수 있어야 한다', async () => {
|
|
382
|
+
await withTestTransaction(async (context) => {
|
|
383
|
+
const { tx } = context.state
|
|
384
|
+
const domain = await domainFactory.create({}, tx)
|
|
385
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
386
|
+
const otherRole = await roleFactory.create({ name: 'Other', domain }, tx)
|
|
387
|
+
|
|
388
|
+
// Given: approverRole을 가진 사용자와 다른 역할을 가진 사용자
|
|
389
|
+
const userWithApproverRole = await userFactory.createWithRole({}, approverRole, tx)
|
|
390
|
+
const userWithOtherRole = await userFactory.createWithRole({}, otherRole, tx)
|
|
391
|
+
const userWithNoRole = await userFactory.create({}, tx)
|
|
392
|
+
|
|
393
|
+
// 역할 확인 로직 시뮬레이션
|
|
394
|
+
const approverRoleId = approverRole.id
|
|
395
|
+
|
|
396
|
+
// userWithApproverRole: 역할 일치 - 승인 가능
|
|
397
|
+
const hasApproverRole1 = userWithApproverRole.roles?.some(
|
|
398
|
+
(r: any) => r.id === approverRoleId
|
|
399
|
+
)
|
|
400
|
+
expect(hasApproverRole1).toBe(true)
|
|
401
|
+
|
|
402
|
+
// userWithOtherRole: 역할 불일치 - 승인 불가
|
|
403
|
+
const hasApproverRole2 = userWithOtherRole.roles?.some(
|
|
404
|
+
(r: any) => r.id === approverRoleId
|
|
405
|
+
)
|
|
406
|
+
expect(hasApproverRole2).toBe(false)
|
|
407
|
+
|
|
408
|
+
// userWithNoRole: 역할 없음 - 승인 불가
|
|
409
|
+
const hasApproverRole3 = userWithNoRole.roles?.some(
|
|
410
|
+
(r: any) => r.id === approverRoleId
|
|
411
|
+
)
|
|
412
|
+
expect(hasApproverRole3).toBeFalsy()
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
describe('null/undefined roleId 처리', () => {
|
|
418
|
+
it('approvalLine[order].value가 undefined이면 오류가 발생해야 한다', async () => {
|
|
419
|
+
await withTestTransaction(async (context) => {
|
|
420
|
+
const { tx } = context.state
|
|
421
|
+
|
|
422
|
+
// Given: value가 undefined인 승인라인
|
|
423
|
+
const invalidApprovalLine = [
|
|
424
|
+
{ type: 'Role', value: undefined }
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
// When: value 추출
|
|
428
|
+
const order = 0
|
|
429
|
+
const approverRoleId = invalidApprovalLine[order].value
|
|
430
|
+
|
|
431
|
+
// Then: null check 실패
|
|
432
|
+
const shouldThrowError = !approverRoleId
|
|
433
|
+
expect(shouldThrowError).toBe(true)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('approvalLine[order].value가 null이면 오류가 발생해야 한다', async () => {
|
|
438
|
+
await withTestTransaction(async (context) => {
|
|
439
|
+
const { tx } = context.state
|
|
440
|
+
|
|
441
|
+
// Given: value가 null인 승인라인
|
|
442
|
+
const invalidApprovalLine = [
|
|
443
|
+
{ type: 'Role', value: null }
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
// When: value 추출
|
|
447
|
+
const order = 0
|
|
448
|
+
const approverRoleId = invalidApprovalLine[order].value
|
|
449
|
+
|
|
450
|
+
// Then: null check 실패
|
|
451
|
+
const shouldThrowError = !approverRoleId
|
|
452
|
+
expect(shouldThrowError).toBe(true)
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
describe('order 인덱스 경계 검증', () => {
|
|
458
|
+
it('approvalLine.length > order 조건으로 다음 레벨 존재 여부를 확인해야 한다', async () => {
|
|
459
|
+
await withTestTransaction(async (context) => {
|
|
460
|
+
const { tx } = context.state
|
|
461
|
+
const domain = await domainFactory.create({}, tx)
|
|
462
|
+
const role1 = await roleFactory.create({ name: 'Role 1', domain }, tx)
|
|
463
|
+
const role2 = await roleFactory.create({ name: 'Role 2', domain }, tx)
|
|
464
|
+
|
|
465
|
+
// Given: 2단계 승인라인
|
|
466
|
+
const approvalLine = [
|
|
467
|
+
createRoleApprovalLineItem(role1),
|
|
468
|
+
createRoleApprovalLineItem(role2)
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
// order=1 (첫 번째 approval): approvalLine[1] 존재 → 에스컬레이션
|
|
472
|
+
const order1 = 1
|
|
473
|
+
const hasNextLevel1 = approvalLine.length > order1 // 2 > 1 = true
|
|
474
|
+
expect(hasNextLevel1).toBe(true)
|
|
475
|
+
|
|
476
|
+
// order=2 (두 번째 approval): approvalLine[2] 없음 → 최종 승인
|
|
477
|
+
const order2 = 2
|
|
478
|
+
const hasNextLevel2 = approvalLine.length > order2 // 2 > 2 = false
|
|
479
|
+
expect(hasNextLevel2).toBe(false)
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('배열 인덱스 범위를 벗어나면 undefined가 반환되어야 한다', async () => {
|
|
484
|
+
await withTestTransaction(async (context) => {
|
|
485
|
+
const { tx } = context.state
|
|
486
|
+
const domain = await domainFactory.create({}, tx)
|
|
487
|
+
const role = await roleFactory.create({ name: 'Only Role', domain }, tx)
|
|
488
|
+
|
|
489
|
+
// Given: 단일 승인라인
|
|
490
|
+
const approvalLine = [createRoleApprovalLineItem(role)]
|
|
491
|
+
|
|
492
|
+
// Then: 범위 밖 인덱스는 undefined
|
|
493
|
+
expect(approvalLine[0]).toBeDefined()
|
|
494
|
+
expect(approvalLine[1]).toBeUndefined()
|
|
495
|
+
expect(approvalLine[2]).toBeUndefined()
|
|
496
|
+
})
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
})
|