Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { v1 as uuidv1 } from 'uuid';
2
3import getBottle, { type Dependencies } from '../../iocContainer/index.js';
4import { instantiateOpaqueType } from '../../utils/typescript-types.js';
5import {
6 makeSubmissionId,
7 type NormalizedItemData,
8} from '../itemProcessingService/index.js';
9import { type ItemSubmissionWithTypeIdentifier } from '../itemProcessingService/makeItemSubmissionWithTypeIdentifier.js';
10import {
11 type ManualReviewToolService,
12 type ReportHistory,
13} from './manualReviewToolService.js';
14
15describe('Manual Review Tool Service', () => {
16 let mrtService: ManualReviewToolService;
17 let container: Dependencies;
18
19 beforeAll(async () => {
20 // The mutation should be ok here since this is initial setup in a
21 // beforeAll; it doesn't involve reset state for each test in the suite
22 /* eslint-disable better-mutation/no-mutation */
23 ({ container } = await getBottle());
24 mrtService = container.ManualReviewToolService;
25 /* eslint-enable better-mutation/no-mutation */
26 });
27
28 afterAll(async () => {
29 await container.closeSharedResourcesForShutdown();
30 });
31
32 // Test that we can start the stalled jobs checker for manual job processing
33 test('should be able to start stalled jobs checker', async () => {
34 const worker = await mrtService['queueOps']['getBullWorker']({
35 orgId: 'dummyOrg',
36 queueId: 'dummyQueue',
37 });
38 // The startStalledCheckTimer method should be available and not throw
39 expect(worker).toBeDefined();
40 });
41
42 // TODO: rework when we rework the MRT error handling
43 test.skip('MRT throws for submitting a job that has already been moved to completed', async () => {
44 const orgId = 'e7c89ce7729',
45 queueId = '1',
46 reviewerId = uuidv1(),
47 reviewerEmail = 'test@test.com',
48 itemId = uuidv1(),
49 itemTypeId = uuidv1();
50
51 await mrtService['queueOps']['addJob']({
52 queueId,
53 enqueueSourceInfo: { kind: 'REPORT' },
54 jobPayload: {
55 createdAt: new Date(),
56 payload: {
57 kind: 'DEFAULT',
58 reportHistory: [],
59 item: instantiateOpaqueType<ItemSubmissionWithTypeIdentifier>({
60 submissionId: makeSubmissionId(),
61 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
62 data: {} as NormalizedItemData,
63 itemTypeIdentifier: {
64 id: itemTypeId,
65 version: new Date().toISOString(),
66 schemaVariant: 'original',
67 },
68 creator: {
69 id: uuidv1(),
70 typeId: uuidv1(),
71 },
72 itemId,
73 }),
74 reportedForReason: undefined,
75 reportedForReasons: [],
76 enqueueSourceInfo: { kind: 'REPORT' },
77 },
78 policyIds: [],
79 },
80 orgId,
81 });
82
83 const dequeuedJob = await mrtService.dequeueNextJob({
84 orgId,
85 queueId,
86 userId: reviewerId,
87 });
88
89 if (!dequeuedJob) {
90 throw new Error('should have dequeued successfully.');
91 }
92
93 await mrtService.submitDecision({
94 queueId,
95 reportHistory: [],
96 jobId: dequeuedJob.job.id,
97 lockToken: dequeuedJob.lockToken,
98 decisionComponents: [
99 {
100 type: 'CUSTOM_ACTION',
101 actions: [{ id: '8481310e8c4' }],
102 policies: [],
103 itemIds: [itemId],
104 itemTypeId,
105 },
106 ],
107 relatedActions: [],
108 reviewerId,
109 reviewerEmail,
110 orgId,
111 });
112
113 const duplicativeDecision = async () => {
114 return mrtService.submitDecision({
115 queueId,
116 reportHistory: [],
117 jobId: dequeuedJob.job.id,
118 lockToken: dequeuedJob.lockToken,
119 decisionComponents: [
120 {
121 type: 'CUSTOM_ACTION',
122 actions: [{ id: '8481310e8c4' }],
123 policies: [],
124 itemIds: [itemId],
125 itemTypeId,
126 },
127 ],
128 relatedActions: [],
129 reviewerId,
130 reviewerEmail,
131 orgId,
132 });
133 };
134
135 await expect(duplicativeDecision()).rejects.toThrow(
136 `No job with ID ${dequeuedJob.job.id} in queue with ID ${queueId}`,
137 );
138 });
139
140 describe('duplicate decision handling', () => {
141 function makeDummyJob() {
142 return {
143 createdAt: new Date(),
144 policyIds: [] as string[],
145 payload: {
146 kind: 'DEFAULT',
147 reportHistory: [] as ReportHistory,
148 item: instantiateOpaqueType<ItemSubmissionWithTypeIdentifier>({
149 submissionId: makeSubmissionId(),
150 submissionTime: new Date(),
151 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
152 data: {} as NormalizedItemData,
153 itemTypeIdentifier: {
154 id: uuidv1(),
155 version: new Date().toISOString(),
156 schemaVariant: 'original',
157 },
158 creator: {
159 id: uuidv1(),
160 typeId: uuidv1(),
161 },
162 itemId: uuidv1(),
163 }),
164 enqueueSourceInfo: { kind: 'REPORT' },
165 },
166 } as const;
167 }
168
169 it('should reject duplicate decisions with the same lock token', async () => {
170 const orgId = 'e7c89ce7729',
171 queueId = '1',
172 reviewerId = uuidv1(),
173 reviewerEmail = 'test@test.com',
174 jobPayload = makeDummyJob();
175 const itemId = jobPayload.payload.item.itemId,
176 itemTypeId = jobPayload.payload.item.itemTypeIdentifier.id;
177
178 await mrtService['queueOps']['addJob']({
179 jobPayload,
180 orgId,
181 queueId,
182 enqueueSourceInfo: { kind: 'REPORT' },
183 });
184
185 const dequeuedJob = await mrtService.dequeueNextJob({
186 orgId,
187 queueId,
188 userId: reviewerId,
189 });
190
191 if (!dequeuedJob) {
192 throw new Error("should've returned a job");
193 }
194
195 await mrtService.submitDecision({
196 queueId,
197 reportHistory: [],
198 jobId: dequeuedJob.job.id,
199 lockToken: dequeuedJob.lockToken,
200 decisionComponents: [
201 {
202 type: 'CUSTOM_ACTION',
203 actions: [{ id: '8481310e8c4' }],
204 policies: [],
205 itemIds: [itemId],
206 itemTypeId,
207 },
208 ],
209 relatedActions: [],
210 reviewerId,
211 reviewerEmail,
212 orgId,
213 });
214
215 const duplicativeDecision = async () => {
216 await mrtService.submitDecision({
217 queueId,
218 reportHistory: [],
219 jobId: dequeuedJob.job.id,
220 lockToken: dequeuedJob.lockToken,
221 decisionComponents: [
222 {
223 type: 'CUSTOM_ACTION',
224 actions: [{ id: '8481310e8c4' }],
225 policies: [],
226 itemIds: [itemId],
227 itemTypeId,
228 },
229 ],
230 relatedActions: [],
231 reviewerId,
232 reviewerEmail,
233 orgId,
234 });
235 };
236
237 await expect(duplicativeDecision()).rejects.toThrow();
238 });
239
240 it.skip('should reject duplicate decisions on jobs dequeued again after the lock expires', async () => {});
241 });
242});