@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,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ROLE Approval Edge Cases and Error Conditions Tests
|
|
3
|
+
* ROLE 승인 엣지 케이스 및 오류 조건 테스트
|
|
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
|
+
activityInstanceFactory,
|
|
13
|
+
activityThreadFactory,
|
|
14
|
+
activityApprovalFactory
|
|
15
|
+
} from '../../../../test/factories'
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
ActivityInstanceStatus,
|
|
19
|
+
ActivityThreadStatus,
|
|
20
|
+
ActivityApprovalJudgment
|
|
21
|
+
} from '../../../../test/entities/schemas'
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
createRoleApprovalLineItem,
|
|
25
|
+
createUserApprovalLineItem
|
|
26
|
+
} from '../../../../test/helpers/workflow-helpers'
|
|
27
|
+
|
|
28
|
+
describe('ROLE Approval Edge Cases and Error Conditions', () => {
|
|
29
|
+
let testDb: TestDatabase
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
testDb = TestDatabase.getInstance()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('빈 승인라인', () => {
|
|
36
|
+
it('승인라인이 undefined이면 승인 프로세스를 건너뛰어야 한다', async () => {
|
|
37
|
+
await withTestTransaction(async (context) => {
|
|
38
|
+
const { tx } = context.state
|
|
39
|
+
const domain = await domainFactory.create({}, tx)
|
|
40
|
+
const user = await userFactory.create({}, tx)
|
|
41
|
+
|
|
42
|
+
// Given: 승인라인이 없는 Instance
|
|
43
|
+
const instance = await activityInstanceFactory.createWithActivity(
|
|
44
|
+
{ state: ActivityInstanceStatus.Started },
|
|
45
|
+
undefined,
|
|
46
|
+
domain,
|
|
47
|
+
tx
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// approvalLine 체크
|
|
51
|
+
const approvalLine = instance.approvalLine
|
|
52
|
+
const hasApprovalLine = approvalLine && approvalLine.length > 0
|
|
53
|
+
expect(hasApprovalLine).toBeFalsy()
|
|
54
|
+
|
|
55
|
+
// Then: Submit 시 즉시 Ended
|
|
56
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
57
|
+
{ state: ActivityThreadStatus.Started },
|
|
58
|
+
instance,
|
|
59
|
+
user,
|
|
60
|
+
tx
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
thread.state = ActivityThreadStatus.Ended
|
|
64
|
+
thread.terminatedAt = new Date()
|
|
65
|
+
const ended = await tx.save('ActivityThread', thread) as any
|
|
66
|
+
|
|
67
|
+
expect(ended.state).toBe(ActivityThreadStatus.Ended)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('승인라인이 빈 배열이면 승인 프로세스를 건너뛰어야 한다', async () => {
|
|
72
|
+
await withTestTransaction(async (context) => {
|
|
73
|
+
const { tx } = context.state
|
|
74
|
+
const domain = await domainFactory.create({}, tx)
|
|
75
|
+
const user = await userFactory.create({}, tx)
|
|
76
|
+
|
|
77
|
+
// Given: 빈 배열 승인라인
|
|
78
|
+
const instance = await activityInstanceFactory.createWithApprovalLine(
|
|
79
|
+
[],
|
|
80
|
+
{ state: ActivityInstanceStatus.Started },
|
|
81
|
+
undefined,
|
|
82
|
+
domain,
|
|
83
|
+
tx
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const approvalLine = instance.approvalLine
|
|
87
|
+
const hasApprovalLine = approvalLine && approvalLine.length > 0
|
|
88
|
+
expect(hasApprovalLine).toBe(false)
|
|
89
|
+
|
|
90
|
+
// Then: Submit 시 즉시 Ended
|
|
91
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
92
|
+
{ state: ActivityThreadStatus.Started },
|
|
93
|
+
instance,
|
|
94
|
+
user,
|
|
95
|
+
tx
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
thread.state = ActivityThreadStatus.Ended
|
|
99
|
+
thread.terminatedAt = new Date()
|
|
100
|
+
const ended = await tx.save('ActivityThread', thread) as any
|
|
101
|
+
|
|
102
|
+
expect(ended.state).toBe(ActivityThreadStatus.Ended)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('존재하지 않는 Role', () => {
|
|
108
|
+
it('삭제된 roleId를 참조하면 Role을 찾을 수 없어야 한다', async () => {
|
|
109
|
+
await withTestTransaction(async (context) => {
|
|
110
|
+
const { tx } = context.state
|
|
111
|
+
const domain = await domainFactory.create({}, tx)
|
|
112
|
+
|
|
113
|
+
// Given: 존재하지 않는 roleId
|
|
114
|
+
const nonExistentRoleId = 'non-existent-role-id'
|
|
115
|
+
const invalidApprovalLine = [
|
|
116
|
+
{ type: 'Role', value: nonExistentRoleId }
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
// When: Role 조회
|
|
120
|
+
const role = await tx.findOne('Role', { where: { id: nonExistentRoleId } })
|
|
121
|
+
|
|
122
|
+
// Then: Role이 없음
|
|
123
|
+
expect(role).toBeNull()
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('역할에 사용자가 없는 경우', () => {
|
|
129
|
+
it('사용자가 없는 Role로도 approval이 생성될 수 있어야 한다', async () => {
|
|
130
|
+
await withTestTransaction(async (context) => {
|
|
131
|
+
const { tx } = context.state
|
|
132
|
+
const domain = await domainFactory.create({}, tx)
|
|
133
|
+
|
|
134
|
+
// Given: 사용자가 할당되지 않은 빈 역할
|
|
135
|
+
const emptyRole = await roleFactory.create({ name: 'Empty Role', domain }, tx)
|
|
136
|
+
|
|
137
|
+
// When: ROLE 기반 approval 생성
|
|
138
|
+
const approval = await activityApprovalFactory.createWithRole(
|
|
139
|
+
{ judgment: ActivityApprovalJudgment.Pending },
|
|
140
|
+
undefined,
|
|
141
|
+
emptyRole,
|
|
142
|
+
tx
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Then: approval은 생성됨 (나중에 역할에 사용자가 추가될 수 있음)
|
|
146
|
+
expect(approval).toBeDefined()
|
|
147
|
+
expect(approval.approverRole?.id).toBe(emptyRole.id)
|
|
148
|
+
expect(approval.approver).toBeUndefined()
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('동일 approval 중복 처리 방지', () => {
|
|
154
|
+
it('이미 Approved된 approval을 다시 승인하면 거부되어야 한다', async () => {
|
|
155
|
+
await withTestTransaction(async (context) => {
|
|
156
|
+
const { tx } = context.state
|
|
157
|
+
const domain = await domainFactory.create({}, tx)
|
|
158
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
159
|
+
|
|
160
|
+
// Given: 이미 Approved 상태인 approval
|
|
161
|
+
const approval = await activityApprovalFactory.createWithRole(
|
|
162
|
+
{
|
|
163
|
+
judgment: ActivityApprovalJudgment.Approved,
|
|
164
|
+
terminatedAt: new Date()
|
|
165
|
+
},
|
|
166
|
+
undefined,
|
|
167
|
+
approverRole,
|
|
168
|
+
tx
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// Then: Pending이 아니므로 재승인 불가
|
|
172
|
+
const canApprove = approval.judgment === ActivityApprovalJudgment.Pending
|
|
173
|
+
expect(canApprove).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('이미 Rejected된 approval을 다시 승인하면 거부되어야 한다', async () => {
|
|
178
|
+
await withTestTransaction(async (context) => {
|
|
179
|
+
const { tx } = context.state
|
|
180
|
+
const domain = await domainFactory.create({}, tx)
|
|
181
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
182
|
+
|
|
183
|
+
// Given: 이미 Rejected 상태인 approval
|
|
184
|
+
const approval = await activityApprovalFactory.createWithRole(
|
|
185
|
+
{
|
|
186
|
+
judgment: ActivityApprovalJudgment.Rejected,
|
|
187
|
+
terminatedAt: new Date()
|
|
188
|
+
},
|
|
189
|
+
undefined,
|
|
190
|
+
approverRole,
|
|
191
|
+
tx
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Then: Pending이 아니므로 재승인 불가
|
|
195
|
+
const canApprove = approval.judgment === ActivityApprovalJudgment.Pending
|
|
196
|
+
expect(canApprove).toBe(false)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('이미 Escalated된 approval을 다시 처리하면 거부되어야 한다', async () => {
|
|
201
|
+
await withTestTransaction(async (context) => {
|
|
202
|
+
const { tx } = context.state
|
|
203
|
+
const domain = await domainFactory.create({}, tx)
|
|
204
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
205
|
+
|
|
206
|
+
// Given: 이미 Escalated 상태인 approval
|
|
207
|
+
const approval = await activityApprovalFactory.createWithRole(
|
|
208
|
+
{
|
|
209
|
+
judgment: ActivityApprovalJudgment.Escalated,
|
|
210
|
+
terminatedAt: new Date()
|
|
211
|
+
},
|
|
212
|
+
undefined,
|
|
213
|
+
approverRole,
|
|
214
|
+
tx
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// Then: Pending이 아니므로 재처리 불가
|
|
218
|
+
const canApprove = approval.judgment === ActivityApprovalJudgment.Pending
|
|
219
|
+
expect(canApprove).toBe(false)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('스레드 상태 검증', () => {
|
|
225
|
+
it('Unassigned 상태 스레드는 submit할 수 없어야 한다', async () => {
|
|
226
|
+
await withTestTransaction(async (context) => {
|
|
227
|
+
const { tx } = context.state
|
|
228
|
+
const domain = await domainFactory.create({}, tx)
|
|
229
|
+
|
|
230
|
+
// Given: Unassigned 상태 스레드 (assignee 없이 생성)
|
|
231
|
+
const instance = await activityInstanceFactory.createWithActivity(
|
|
232
|
+
{ state: ActivityInstanceStatus.Issued },
|
|
233
|
+
undefined,
|
|
234
|
+
domain,
|
|
235
|
+
tx
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const thread = await tx.save('ActivityThread', {
|
|
239
|
+
state: ActivityThreadStatus.Unassigned,
|
|
240
|
+
round: 1,
|
|
241
|
+
activityInstance: instance,
|
|
242
|
+
domain
|
|
243
|
+
}) as any
|
|
244
|
+
|
|
245
|
+
// Then: Submit 불가능한 상태
|
|
246
|
+
const canSubmit =
|
|
247
|
+
thread.state === ActivityThreadStatus.Assigned ||
|
|
248
|
+
thread.state === ActivityThreadStatus.Started
|
|
249
|
+
expect(canSubmit).toBe(false)
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('Submitted 상태 스레드는 다시 submit할 수 없어야 한다', async () => {
|
|
254
|
+
await withTestTransaction(async (context) => {
|
|
255
|
+
const { tx } = context.state
|
|
256
|
+
const domain = await domainFactory.create({}, tx)
|
|
257
|
+
const user = await userFactory.create({}, tx)
|
|
258
|
+
|
|
259
|
+
// Given: Submitted 상태 스레드
|
|
260
|
+
const instance = await activityInstanceFactory.createWithActivity(
|
|
261
|
+
{ state: ActivityInstanceStatus.Started },
|
|
262
|
+
undefined,
|
|
263
|
+
domain,
|
|
264
|
+
tx
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
268
|
+
{ state: ActivityThreadStatus.Submitted },
|
|
269
|
+
instance,
|
|
270
|
+
user,
|
|
271
|
+
tx
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
// Then: Submit 불가능한 상태
|
|
275
|
+
const canSubmit =
|
|
276
|
+
thread.state === ActivityThreadStatus.Assigned ||
|
|
277
|
+
thread.state === ActivityThreadStatus.Started
|
|
278
|
+
expect(canSubmit).toBe(false)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('order 경계 조건', () => {
|
|
284
|
+
it('order가 0이면 잘못된 상태여야 한다', async () => {
|
|
285
|
+
await withTestTransaction(async (context) => {
|
|
286
|
+
const { tx } = context.state
|
|
287
|
+
const domain = await domainFactory.create({}, tx)
|
|
288
|
+
const role = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
289
|
+
|
|
290
|
+
// order는 1부터 시작해야 함
|
|
291
|
+
const invalidOrder = 0
|
|
292
|
+
const validOrders = [1, 2, 3]
|
|
293
|
+
|
|
294
|
+
expect(validOrders.includes(invalidOrder)).toBe(false)
|
|
295
|
+
expect(invalidOrder).toBeLessThan(1)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('order가 음수이면 잘못된 상태여야 한다', async () => {
|
|
300
|
+
await withTestTransaction(async (context) => {
|
|
301
|
+
const { tx } = context.state
|
|
302
|
+
|
|
303
|
+
const invalidOrder = -1
|
|
304
|
+
expect(invalidOrder).toBeLessThan(1)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('order가 approvalLine 길이보다 크면 더 이상 에스컬레이션이 없어야 한다', async () => {
|
|
309
|
+
await withTestTransaction(async (context) => {
|
|
310
|
+
const { tx } = context.state
|
|
311
|
+
const domain = await domainFactory.create({}, tx)
|
|
312
|
+
const role = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
313
|
+
|
|
314
|
+
// Given: 단일 승인라인
|
|
315
|
+
const approvalLine = [createRoleApprovalLineItem(role)]
|
|
316
|
+
|
|
317
|
+
// order=1일 때: approvalLine.length(1) > order(1) = false
|
|
318
|
+
const order = 1
|
|
319
|
+
const hasNextLevel = approvalLine.length > order
|
|
320
|
+
expect(hasNextLevel).toBe(false)
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe('타입스탬프 설정', () => {
|
|
326
|
+
it('승인 완료 시 terminatedAt이 반드시 설정되어야 한다', async () => {
|
|
327
|
+
await withTestTransaction(async (context) => {
|
|
328
|
+
const { tx } = context.state
|
|
329
|
+
const domain = await domainFactory.create({}, tx)
|
|
330
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
331
|
+
const approver = await userFactory.createWithRole({}, approverRole, tx)
|
|
332
|
+
|
|
333
|
+
// Given: Pending approval
|
|
334
|
+
const approval = await activityApprovalFactory.createWithRole(
|
|
335
|
+
{ judgment: ActivityApprovalJudgment.Pending },
|
|
336
|
+
undefined,
|
|
337
|
+
approverRole,
|
|
338
|
+
tx
|
|
339
|
+
)
|
|
340
|
+
expect(approval.terminatedAt).toBeFalsy() // null or undefined
|
|
341
|
+
|
|
342
|
+
// When: 승인
|
|
343
|
+
approval.judgment = ActivityApprovalJudgment.Approved
|
|
344
|
+
approval.approver = approver
|
|
345
|
+
approval.terminatedAt = new Date()
|
|
346
|
+
const approved = await tx.save('ActivityApproval', approval) as any
|
|
347
|
+
|
|
348
|
+
// Then
|
|
349
|
+
expect(approved.terminatedAt).toBeDefined()
|
|
350
|
+
expect(approved.terminatedAt).toBeInstanceOf(Date)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('반려 시 terminatedAt이 반드시 설정되어야 한다', async () => {
|
|
355
|
+
await withTestTransaction(async (context) => {
|
|
356
|
+
const { tx } = context.state
|
|
357
|
+
const domain = await domainFactory.create({}, tx)
|
|
358
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
359
|
+
const approver = await userFactory.createWithRole({}, approverRole, tx)
|
|
360
|
+
|
|
361
|
+
// Given: Pending approval
|
|
362
|
+
const approval = await activityApprovalFactory.createWithRole(
|
|
363
|
+
{ judgment: ActivityApprovalJudgment.Pending },
|
|
364
|
+
undefined,
|
|
365
|
+
approverRole,
|
|
366
|
+
tx
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
// When: 반려
|
|
370
|
+
approval.judgment = ActivityApprovalJudgment.Rejected
|
|
371
|
+
approval.approver = approver
|
|
372
|
+
approval.comment = 'Rejected reason'
|
|
373
|
+
approval.terminatedAt = new Date()
|
|
374
|
+
const rejected = await tx.save('ActivityApproval', approval) as any
|
|
375
|
+
|
|
376
|
+
// Then
|
|
377
|
+
expect(rejected.terminatedAt).toBeDefined()
|
|
378
|
+
expect(rejected.terminatedAt).toBeInstanceOf(Date)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
describe('Domain 격리', () => {
|
|
384
|
+
it('다른 Domain의 Role로 approval을 생성할 수 없어야 한다 (개념적)', async () => {
|
|
385
|
+
await withTestTransaction(async (context) => {
|
|
386
|
+
const { tx } = context.state
|
|
387
|
+
|
|
388
|
+
// Given: 두 개의 다른 Domain
|
|
389
|
+
const domain1 = await domainFactory.create({ name: 'Domain 1' }, tx)
|
|
390
|
+
const domain2 = await domainFactory.create({ name: 'Domain 2' }, tx)
|
|
391
|
+
|
|
392
|
+
// Given: 각 도메인의 역할
|
|
393
|
+
const role1 = await roleFactory.create({ name: 'Role in Domain 1', domain: domain1 }, tx)
|
|
394
|
+
const role2 = await roleFactory.create({ name: 'Role in Domain 2', domain: domain2 }, tx)
|
|
395
|
+
|
|
396
|
+
// Then: 역할은 각 도메인에 속함
|
|
397
|
+
expect(role1.domain?.id).toBe(domain1.id)
|
|
398
|
+
expect(role2.domain?.id).toBe(domain2.id)
|
|
399
|
+
|
|
400
|
+
// 실제 구현에서는 Domain 격리 검증이 필요
|
|
401
|
+
expect(role1.domain?.id).not.toBe(domain2.id)
|
|
402
|
+
expect(role2.domain?.id).not.toBe(domain1.id)
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('Round 증가 규칙', () => {
|
|
408
|
+
it('Rejected 후 재시작 시 round가 증가해야 한다', async () => {
|
|
409
|
+
await withTestTransaction(async (context) => {
|
|
410
|
+
const { tx } = context.state
|
|
411
|
+
const domain = await domainFactory.create({}, tx)
|
|
412
|
+
const user = await userFactory.create({}, tx)
|
|
413
|
+
|
|
414
|
+
// Given: Round 1 스레드
|
|
415
|
+
const instance = await activityInstanceFactory.createWithActivity(
|
|
416
|
+
{ state: ActivityInstanceStatus.Started },
|
|
417
|
+
undefined,
|
|
418
|
+
domain,
|
|
419
|
+
tx
|
|
420
|
+
)
|
|
421
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
422
|
+
{ state: ActivityThreadStatus.Started, round: 1 },
|
|
423
|
+
instance,
|
|
424
|
+
user,
|
|
425
|
+
tx
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
// When: 반려됨
|
|
429
|
+
thread.state = ActivityThreadStatus.Rejected
|
|
430
|
+
const rejected = await tx.save('ActivityThread', thread) as any
|
|
431
|
+
|
|
432
|
+
// When: 재시작
|
|
433
|
+
rejected.state = ActivityThreadStatus.Started
|
|
434
|
+
rejected.round = rejected.round + 1
|
|
435
|
+
const restarted = await tx.save('ActivityThread', rejected) as any
|
|
436
|
+
|
|
437
|
+
// Then: Round 증가
|
|
438
|
+
expect(restarted.round).toBe(2)
|
|
439
|
+
})
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('동일 round에서 approval의 round도 일치해야 한다', async () => {
|
|
443
|
+
await withTestTransaction(async (context) => {
|
|
444
|
+
const { tx } = context.state
|
|
445
|
+
const domain = await domainFactory.create({}, tx)
|
|
446
|
+
const user = await userFactory.create({}, tx)
|
|
447
|
+
const approverRole = await roleFactory.create({ name: 'Approver', domain }, tx)
|
|
448
|
+
|
|
449
|
+
// Given: Round 2 스레드
|
|
450
|
+
const instance = await activityInstanceFactory.createWithActivity(
|
|
451
|
+
{ state: ActivityInstanceStatus.Started },
|
|
452
|
+
undefined,
|
|
453
|
+
domain,
|
|
454
|
+
tx
|
|
455
|
+
)
|
|
456
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
457
|
+
{ state: ActivityThreadStatus.Submitted, round: 2 },
|
|
458
|
+
instance,
|
|
459
|
+
user,
|
|
460
|
+
tx
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
// When: Approval 생성
|
|
464
|
+
const approval = await tx.save('ActivityApproval', {
|
|
465
|
+
domain,
|
|
466
|
+
activityThread: thread,
|
|
467
|
+
round: thread.round, // 스레드 round와 일치
|
|
468
|
+
order: 1,
|
|
469
|
+
judgment: ActivityApprovalJudgment.Pending,
|
|
470
|
+
approverRole: approverRole,
|
|
471
|
+
creator: user,
|
|
472
|
+
updater: user
|
|
473
|
+
}) as any
|
|
474
|
+
|
|
475
|
+
// Then: Round 일치
|
|
476
|
+
expect(approval.round).toBe(thread.round)
|
|
477
|
+
expect(approval.round).toBe(2)
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
describe('ActivityApprovalJudgment 값 검증', () => {
|
|
483
|
+
it('Pending은 빈 문자열이어야 한다', () => {
|
|
484
|
+
expect(ActivityApprovalJudgment.Pending).toBe('')
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('유효한 judgment 값만 허용되어야 한다', () => {
|
|
488
|
+
const validJudgments = [
|
|
489
|
+
ActivityApprovalJudgment.Pending,
|
|
490
|
+
ActivityApprovalJudgment.Rejected,
|
|
491
|
+
ActivityApprovalJudgment.Escalated,
|
|
492
|
+
ActivityApprovalJudgment.Delegated,
|
|
493
|
+
ActivityApprovalJudgment.Approved,
|
|
494
|
+
ActivityApprovalJudgment.Aborted
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
expect(validJudgments).toContain('')
|
|
498
|
+
expect(validJudgments).toContain('rejected')
|
|
499
|
+
expect(validJudgments).toContain('escalated')
|
|
500
|
+
expect(validJudgments).toContain('delegated')
|
|
501
|
+
expect(validJudgments).toContain('approved')
|
|
502
|
+
expect(validJudgments).toContain('aborted')
|
|
503
|
+
|
|
504
|
+
expect(validJudgments).not.toContain('invalid')
|
|
505
|
+
expect(validJudgments).not.toContain('Approved') // 대소문자 구분
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
describe('OOC Resolve 시나리오 (버그 수정 주요 케이스)', () => {
|
|
510
|
+
it('OOC Resolve에서 ROLE 타입 outlierApprovalLine이 정상 작동해야 한다', async () => {
|
|
511
|
+
await withTestTransaction(async (context) => {
|
|
512
|
+
const { tx } = context.state
|
|
513
|
+
const domain = await domainFactory.create({}, tx)
|
|
514
|
+
const oocApproverRole = await roleFactory.create({ name: 'OOC Approver', domain }, tx)
|
|
515
|
+
const resolver = await userFactory.create({ name: 'Resolver' }, tx)
|
|
516
|
+
const approver = await userFactory.createWithRole({ name: 'Approver' }, oocApproverRole, tx)
|
|
517
|
+
|
|
518
|
+
// Given: OOC Resolve Activity with ROLE-based outlierApprovalLine
|
|
519
|
+
const outlierApprovalLine = [createRoleApprovalLineItem(oocApproverRole)]
|
|
520
|
+
|
|
521
|
+
const instance = await activityInstanceFactory.createWithApprovalLine(
|
|
522
|
+
outlierApprovalLine,
|
|
523
|
+
{
|
|
524
|
+
name: '[OOC 조치]',
|
|
525
|
+
state: ActivityInstanceStatus.Started
|
|
526
|
+
},
|
|
527
|
+
undefined,
|
|
528
|
+
domain,
|
|
529
|
+
tx
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
expect(instance.approvalLine?.[0].type).toBe('Role')
|
|
533
|
+
expect(instance.approvalLine?.[0].value).toBe(oocApproverRole.id)
|
|
534
|
+
|
|
535
|
+
// Given: Resolver가 작업 완료 후 Submit
|
|
536
|
+
const thread = await activityThreadFactory.createWithInstanceAndAssignee(
|
|
537
|
+
{ state: ActivityThreadStatus.Submitted },
|
|
538
|
+
instance,
|
|
539
|
+
resolver,
|
|
540
|
+
tx
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
// When: ROLE 기반 approval 생성 (버그 수정 후 정상 작동)
|
|
544
|
+
// 수정 전: if (approverRoleId) throw - roleId가 있으면 항상 에러
|
|
545
|
+
// 수정 후: if (!approverRoleId) throw - roleId가 없을 때만 에러
|
|
546
|
+
const approvalLineItem = outlierApprovalLine[0]
|
|
547
|
+
const approverRoleId = approvalLineItem.value
|
|
548
|
+
|
|
549
|
+
// 버그 수정 확인: roleId가 있으므로 정상 처리되어야 함
|
|
550
|
+
expect(approverRoleId).toBeDefined()
|
|
551
|
+
expect(approverRoleId).toBe(oocApproverRole.id)
|
|
552
|
+
|
|
553
|
+
const approval = await tx.save('ActivityApproval', {
|
|
554
|
+
domain,
|
|
555
|
+
activityThread: thread,
|
|
556
|
+
round: 1,
|
|
557
|
+
order: 1,
|
|
558
|
+
judgment: ActivityApprovalJudgment.Pending,
|
|
559
|
+
approverRole: oocApproverRole,
|
|
560
|
+
transaction: 'submit',
|
|
561
|
+
creator: resolver,
|
|
562
|
+
updater: resolver
|
|
563
|
+
}) as any
|
|
564
|
+
|
|
565
|
+
expect(approval).toBeDefined()
|
|
566
|
+
expect(approval.approverRole?.id).toBe(oocApproverRole.id)
|
|
567
|
+
|
|
568
|
+
// When: 해당 역할 소유자가 승인
|
|
569
|
+
approval.judgment = ActivityApprovalJudgment.Approved
|
|
570
|
+
approval.approver = approver
|
|
571
|
+
approval.terminatedAt = new Date()
|
|
572
|
+
const approved = await tx.save('ActivityApproval', approval) as any
|
|
573
|
+
|
|
574
|
+
// Then: OOC Resolve 승인 완료
|
|
575
|
+
expect(approved.judgment).toBe(ActivityApprovalJudgment.Approved)
|
|
576
|
+
expect(approved.approver?.id).toBe(approver.id)
|
|
577
|
+
expect(approved.approverRole?.id).toBe(oocApproverRole.id)
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
})
|