@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.
Files changed (103) hide show
  1. package/dist-client/components/activity-thread-timeline.d.ts +1 -9
  2. package/dist-client/components/activity-thread-timeline.js +1 -3
  3. package/dist-client/components/activity-thread-timeline.js.map +1 -1
  4. package/dist-client/pages/activity/activity-list-page.d.ts +1 -7
  5. package/dist-client/pages/activity/activity-list-page.js +125 -234
  6. package/dist-client/pages/activity/activity-list-page.js.map +1 -1
  7. package/dist-client/pages/activity/activity-page.d.ts +1 -7
  8. package/dist-client/pages/activity/activity-page.js +51 -93
  9. package/dist-client/pages/activity/activity-page.js.map +1 -1
  10. package/dist-client/pages/activity/starter-list-page.d.ts +1 -7
  11. package/dist-client/pages/activity/starter-list-page.js +33 -62
  12. package/dist-client/pages/activity/starter-list-page.js.map +1 -1
  13. package/dist-client/pages/activity-approval/activity-approval-list-page.d.ts +1 -7
  14. package/dist-client/pages/activity-approval/activity-approval-list-page.js +50 -95
  15. package/dist-client/pages/activity-approval/activity-approval-list-page.js.map +1 -1
  16. package/dist-client/pages/activity-approval/activity-approval-page.d.ts +1 -7
  17. package/dist-client/pages/activity-approval/activity-approval-page.js +73 -119
  18. package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
  19. package/dist-client/pages/activity-instance/activity-instance-list-page.d.ts +0 -6
  20. package/dist-client/pages/activity-instance/activity-instance-list-page.js +63 -120
  21. package/dist-client/pages/activity-instance/activity-instance-list-page.js.map +1 -1
  22. package/dist-client/pages/activity-instance/activity-instance-search-page.d.ts +1 -7
  23. package/dist-client/pages/activity-instance/activity-instance-search-page.js +55 -101
  24. package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
  25. package/dist-client/pages/activity-instance/activity-instance-start-page.d.ts +1 -7
  26. package/dist-client/pages/activity-instance/activity-instance-start-page.js +65 -109
  27. package/dist-client/pages/activity-instance/activity-instance-start-page.js.map +1 -1
  28. package/dist-client/pages/activity-stats/activity-stats-list-page.d.ts +1 -7
  29. package/dist-client/pages/activity-stats/activity-stats-list-page.js +50 -95
  30. package/dist-client/pages/activity-stats/activity-stats-list-page.js.map +1 -1
  31. package/dist-client/pages/activity-store/activity-store-page.d.ts +1 -7
  32. package/dist-client/pages/activity-store/activity-store-page.js +2 -3
  33. package/dist-client/pages/activity-store/activity-store-page.js.map +1 -1
  34. package/dist-client/pages/activity-supervisor/reporter-list-page.d.ts +1 -7
  35. package/dist-client/pages/activity-supervisor/reporter-list-page.js +36 -66
  36. package/dist-client/pages/activity-supervisor/reporter-list-page.js.map +1 -1
  37. package/dist-client/pages/activity-template/activity-template-list-page.d.ts +1 -7
  38. package/dist-client/pages/activity-template/activity-template-list-page.js +70 -134
  39. package/dist-client/pages/activity-template/activity-template-list-page.js.map +1 -1
  40. package/dist-client/pages/activity-thread/activity-thread-list-page.d.ts +1 -7
  41. package/dist-client/pages/activity-thread/activity-thread-list-page.js +49 -93
  42. package/dist-client/pages/activity-thread/activity-thread-list-page.js.map +1 -1
  43. package/dist-client/pages/activity-thread/activity-thread-page.d.ts +1 -7
  44. package/dist-client/pages/activity-thread/activity-thread-page.js +80 -135
  45. package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
  46. package/dist-client/pages/activity-thread/activity-thread-view-page.d.ts +1 -7
  47. package/dist-client/pages/activity-thread/activity-thread-view-page.js +54 -80
  48. package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
  49. package/dist-client/pages/activity-thread/activity-thread-view.js +4 -0
  50. package/dist-client/pages/activity-thread/activity-thread-view.js.map +1 -1
  51. package/dist-client/pages/dashboard/dashboard-home.js +3 -5
  52. package/dist-client/pages/dashboard/dashboard-home.js.map +1 -1
  53. package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +0 -6
  54. package/dist-client/pages/installable-activity/installable-activity-list-page.js +68 -130
  55. package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
  56. package/dist-client/pages/todo/approval-done-list-page.d.ts +1 -7
  57. package/dist-client/pages/todo/approval-done-list-page.js +53 -100
  58. package/dist-client/pages/todo/approval-done-list-page.js.map +1 -1
  59. package/dist-client/pages/todo/approval-pending-list-page.d.ts +0 -6
  60. package/dist-client/pages/todo/approval-pending-list-page.js +63 -119
  61. package/dist-client/pages/todo/approval-pending-list-page.js.map +1 -1
  62. package/dist-client/pages/todo/done-list-calendar-page.d.ts +1 -7
  63. package/dist-client/pages/todo/done-list-calendar-page.js +2 -3
  64. package/dist-client/pages/todo/done-list-calendar-page.js.map +1 -1
  65. package/dist-client/pages/todo/done-list-page.d.ts +1 -7
  66. package/dist-client/pages/todo/done-list-page.js +56 -106
  67. package/dist-client/pages/todo/done-list-page.js.map +1 -1
  68. package/dist-client/pages/todo/draft-list-page.d.ts +1 -7
  69. package/dist-client/pages/todo/draft-list-page.js +49 -88
  70. package/dist-client/pages/todo/draft-list-page.js.map +1 -1
  71. package/dist-client/pages/todo/pickable-list-page.d.ts +1 -7
  72. package/dist-client/pages/todo/pickable-list-page.js +48 -91
  73. package/dist-client/pages/todo/pickable-list-page.js.map +1 -1
  74. package/dist-client/pages/todo/todo-list-page.d.ts +0 -6
  75. package/dist-client/pages/todo/todo-list-page.js +56 -106
  76. package/dist-client/pages/todo/todo-list-page.js.map +1 -1
  77. package/dist-client/pages/worklist-home.js +2 -3
  78. package/dist-client/pages/worklist-home.js.map +1 -1
  79. package/dist-client/route.d.ts +1 -1
  80. package/dist-client/templates/activity-approval-context-template.js +8 -12
  81. package/dist-client/templates/activity-approval-context-template.js.map +1 -1
  82. package/dist-client/templates/activity-instance-context-template.js +8 -12
  83. package/dist-client/templates/activity-instance-context-template.js.map +1 -1
  84. package/dist-client/templates/activity-thread-context-template.js +8 -12
  85. package/dist-client/templates/activity-thread-context-template.js.map +1 -1
  86. package/dist-client/tsconfig.tsbuildinfo +1 -1
  87. package/dist-server/controllers/activity-approval/approve.js +2 -2
  88. package/dist-server/controllers/activity-approval/approve.js.map +1 -1
  89. package/dist-server/controllers/activity-thread/submit.js +2 -2
  90. package/dist-server/controllers/activity-thread/submit.js.map +1 -1
  91. package/dist-server/service/index.d.ts +2 -2
  92. package/dist-server/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +16 -16
  94. package/spec/integration/approval-mixed-types.spec.ts +491 -0
  95. package/spec/integration/approval-role-based.spec.ts +389 -0
  96. package/spec/integration/instance-lifecycle.spec.ts +406 -0
  97. package/spec/integration/role-approval-edge-cases.spec.ts +581 -0
  98. package/spec/unit/controllers/activity-instance-issue.spec.ts +360 -0
  99. package/spec/unit/controllers/activity-thread-submit.spec.ts +384 -0
  100. package/spec/unit/role-approval-escalate-logic.spec.ts +499 -0
  101. package/spec/unit/role-approval-submit-logic.spec.ts +481 -0
  102. package/spec/unit/thread-state-helpers.spec.ts +253 -0
  103. 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
+ })