Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { v1 as uuidv1 } from 'uuid';
2
3import getBottle from '../../../iocContainer/index.js';
4import { UserPermission } from '../../../models/types/permissioning.js';
5import createOrg from '../../../test/fixtureHelpers/createOrg.js';
6import createUser from '../../../test/fixtureHelpers/createUser.js';
7import { makeTestWithFixture } from '../../../test/utils.js';
8import CommentOperations from './CommentOperations.js';
9
10describe('CommentOperations', () => {
11 const testWithFixtures = makeTestWithFixture(async () => {
12 const container = (await getBottle()).container;
13 const pgQuery = container.KyselyPg;
14 const commentOps = new CommentOperations(pgQuery);
15
16 // Create test org
17 const orgId = uuidv1();
18 const { cleanup: orgCleanup } = await createOrg(
19 { Org: container.Sequelize.Org },
20 container.ModerationConfigService,
21 container.ApiKeyService,
22 orgId,
23 );
24
25 // Create test user
26 const { user, cleanup: userCleanup } = await createUser(
27 container.Sequelize,
28 orgId,
29 );
30
31 // Create a queue (required for job_creations foreign key)
32 const queue = await container.ManualReviewToolService.createManualReviewQueue({
33 name: 'Test Queue',
34 description: null,
35 userIds: [user.id],
36 hiddenActionIds: [],
37 isAppealsQueue: false,
38 invokedBy: {
39 userId: user.id,
40 permissions: [UserPermission.EDIT_MRT_QUEUES],
41 orgId,
42 },
43 });
44
45 // Create test item identifiers and jobs
46 const itemId = uuidv1();
47 const itemTypeId = uuidv1();
48 const jobId1 = uuidv1();
49 const jobId2 = uuidv1();
50
51 await pgQuery
52 .insertInto('manual_review_tool.job_creations')
53 .values([
54 {
55 id: jobId1 as any,
56 org_id: orgId,
57 item_id: itemId,
58 item_type_id: itemTypeId,
59 queue_id: queue.id,
60 created_at: new Date('2023-01-01'),
61 enqueue_source_info: {},
62 },
63 {
64 id: jobId2 as any,
65 org_id: orgId,
66 item_id: itemId,
67 item_type_id: itemTypeId,
68 queue_id: queue.id,
69 created_at: new Date('2023-01-02'),
70 enqueue_source_info: {},
71 },
72 ])
73 .execute();
74
75 return {
76 commentOps,
77 pgQuery,
78 orgId,
79 userId: user.id,
80 itemId,
81 itemTypeId,
82 jobId1,
83 jobId2,
84 queueId: queue.id,
85 async cleanup() {
86 // Clean up comments
87 await pgQuery
88 .deleteFrom('manual_review_tool.job_comments')
89 .where('org_id', '=', orgId)
90 .execute();
91
92 // Clean up job_creations
93 await pgQuery
94 .deleteFrom('manual_review_tool.job_creations')
95 .where('org_id', '=', orgId)
96 .execute();
97
98 // Clean up queue
99 await container.ManualReviewToolService.deleteManualReviewQueueForTestsDO_NOT_USE(
100 orgId,
101 queue.id,
102 );
103
104 // Clean up user and org
105 await userCleanup();
106 await orgCleanup();
107
108 // Close database connections
109 await container.KyselyPg.destroy();
110 await container.KyselyPgReadReplica.destroy();
111 },
112 };
113 });
114
115 describe('getRelatedJobIds', () => {
116 testWithFixtures(
117 'should return single job ID when job not found in job_creations',
118 async ({ commentOps, orgId }) => {
119 const nonExistentJobId = uuidv1();
120
121 // Access private method for testing
122 const result = await (commentOps as any).getRelatedJobIds({
123 orgId,
124 jobId: nonExistentJobId,
125 });
126
127 expect(result).toEqual([nonExistentJobId]);
128 },
129 );
130
131 testWithFixtures(
132 'should return all related job IDs when job found in job_creations',
133 async ({ commentOps, orgId, jobId1, jobId2 }) => {
134 const result = await (commentOps as any).getRelatedJobIds({
135 orgId,
136 jobId: jobId1,
137 });
138
139 expect(result).toHaveLength(2);
140 expect(result).toContain(jobId1);
141 expect(result).toContain(jobId2);
142 },
143 );
144 });
145
146 describe('getComments', () => {
147 testWithFixtures(
148 'should return comments for single job when job not in job_creations',
149 async ({ commentOps, pgQuery, orgId, userId }) => {
150 const singleJobId = uuidv1();
151
152 // Add a comment directly to a job not in job_creations
153 await pgQuery
154 .insertInto('manual_review_tool.job_comments')
155 .values({
156 id: uuidv1(),
157 org_id: orgId,
158 job_id: singleJobId,
159 comment_text: 'Test comment',
160 author_id: userId,
161 created_at: new Date(),
162 })
163 .execute();
164
165 const result = await commentOps.getComments({ orgId, jobId: singleJobId });
166
167 expect(result).toHaveLength(1);
168 expect(result[0].commentText).toBe('Test comment');
169 },
170 );
171
172 testWithFixtures(
173 'should return comments for all related jobs when job found in job_creations',
174 async ({ commentOps, pgQuery, orgId, userId, jobId1, jobId2 }) => {
175 // Add comments to both related jobs
176 await pgQuery
177 .insertInto('manual_review_tool.job_comments')
178 .values([
179 {
180 id: uuidv1(),
181 org_id: orgId,
182 job_id: jobId1,
183 comment_text: 'Comment from first queue',
184 author_id: userId,
185 created_at: new Date('2023-01-01T10:00:00Z'),
186 },
187 {
188 id: uuidv1(),
189 org_id: orgId,
190 job_id: jobId2,
191 comment_text: 'Comment from second queue',
192 author_id: userId,
193 created_at: new Date('2023-01-02T10:00:00Z'),
194 },
195 ])
196 .execute();
197
198 const result = await commentOps.getComments({ orgId, jobId: jobId1 });
199
200 expect(result).toHaveLength(2);
201 expect(result[0].commentText).toBe('Comment from first queue');
202 expect(result[1].commentText).toBe('Comment from second queue');
203 },
204 );
205
206 testWithFixtures(
207 'should return empty array when no comments found',
208 async ({ commentOps, orgId, jobId1 }) => {
209 const result = await commentOps.getComments({ orgId, jobId: jobId1 });
210
211 expect(result).toEqual([]);
212 },
213 );
214
215 testWithFixtures(
216 'should order comments by created_at ascending',
217 async ({ commentOps, pgQuery, orgId, userId, jobId1 }) => {
218 // Add comments with specific timestamps
219 await pgQuery
220 .insertInto('manual_review_tool.job_comments')
221 .values([
222 {
223 id: uuidv1(),
224 org_id: orgId,
225 job_id: jobId1,
226 comment_text: 'Third comment',
227 author_id: userId,
228 created_at: new Date('2023-01-03T10:00:00Z'),
229 },
230 {
231 id: uuidv1(),
232 org_id: orgId,
233 job_id: jobId1,
234 comment_text: 'First comment',
235 author_id: userId,
236 created_at: new Date('2023-01-01T10:00:00Z'),
237 },
238 {
239 id: uuidv1(),
240 org_id: orgId,
241 job_id: jobId1,
242 comment_text: 'Second comment',
243 author_id: userId,
244 created_at: new Date('2023-01-02T10:00:00Z'),
245 },
246 ])
247 .execute();
248
249 const result = await commentOps.getComments({ orgId, jobId: jobId1 });
250
251 expect(result).toHaveLength(3);
252 expect(result[0].commentText).toBe('First comment');
253 expect(result[1].commentText).toBe('Second comment');
254 expect(result[2].commentText).toBe('Third comment');
255 },
256 );
257 });
258
259 describe('getCommentCount', () => {
260 testWithFixtures(
261 'should return 0 when no comments found',
262 async ({ commentOps, orgId, jobId1 }) => {
263 const result = await commentOps.getCommentCount({ orgId, jobId: jobId1 });
264
265 expect(result).toBe(0);
266 },
267 );
268
269 testWithFixtures(
270 'should return correct count for cross-queue comments',
271 async ({ commentOps, pgQuery, orgId, userId, jobId1, jobId2 }) => {
272 // Add comments to both related jobs
273 await pgQuery
274 .insertInto('manual_review_tool.job_comments')
275 .values([
276 {
277 id: uuidv1(),
278 org_id: orgId,
279 job_id: jobId1,
280 comment_text: 'Comment 1',
281 author_id: userId,
282 created_at: new Date(),
283 },
284 {
285 id: uuidv1(),
286 org_id: orgId,
287 job_id: jobId1,
288 comment_text: 'Comment 2',
289 author_id: userId,
290 created_at: new Date(),
291 },
292 {
293 id: uuidv1(),
294 org_id: orgId,
295 job_id: jobId2,
296 comment_text: 'Comment 3',
297 author_id: userId,
298 created_at: new Date(),
299 },
300 ])
301 .execute();
302
303 const result = await commentOps.getCommentCount({ orgId, jobId: jobId1 });
304
305 expect(result).toBe(3);
306 },
307 );
308 });
309
310 describe('addComment', () => {
311 testWithFixtures(
312 'should add comment successfully',
313 async ({ commentOps, orgId, userId, jobId1 }) => {
314 const commentText = 'New test comment';
315
316 const result = await commentOps.addComment({
317 orgId,
318 jobId: jobId1,
319 commentText,
320 authorId: userId,
321 });
322
323 expect(result.commentText).toBe(commentText);
324 expect(result.authorId).toBe(userId);
325 expect(result.id).toBeDefined();
326 expect(result.createdAt).toBeInstanceOf(Date);
327
328 // Verify comment was actually inserted
329 const comments = await commentOps.getComments({ orgId, jobId: jobId1 });
330 expect(comments).toHaveLength(1);
331 expect(comments[0].id).toBe(result.id);
332 },
333 );
334 });
335
336 describe('deleteComment', () => {
337 testWithFixtures(
338 'should delete comment successfully',
339 async ({ commentOps, orgId, userId, jobId1 }) => {
340 // Add a comment first
341 const comment = await commentOps.addComment({
342 orgId,
343 jobId: jobId1,
344 commentText: 'Comment to delete',
345 authorId: userId,
346 });
347
348 const result = await commentOps.deleteComment({
349 orgId,
350 jobId: jobId1,
351 userId,
352 commentId: comment.id,
353 });
354
355 expect(result).toBe(true);
356
357 // Verify comment was actually deleted
358 const comments = await commentOps.getComments({ orgId, jobId: jobId1 });
359 expect(comments).toHaveLength(0);
360 },
361 );
362
363 testWithFixtures(
364 'should return false when comment not found',
365 async ({ commentOps, orgId, userId, jobId1 }) => {
366 const nonExistentCommentId = uuidv1();
367
368 const result = await commentOps.deleteComment({
369 orgId,
370 jobId: jobId1,
371 userId,
372 commentId: nonExistentCommentId,
373 });
374
375 expect(result).toBe(false);
376 },
377 );
378
379 testWithFixtures(
380 'should return false when comment not owned by user',
381 async ({ commentOps, orgId, userId, jobId1 }) => {
382 const otherUserId = uuidv1();
383
384 // Add a comment with a different user
385 const comment = await commentOps.addComment({
386 orgId,
387 jobId: jobId1,
388 commentText: 'Comment by other user',
389 authorId: otherUserId,
390 });
391
392 // Try to delete with wrong user
393 const result = await commentOps.deleteComment({
394 orgId,
395 jobId: jobId1,
396 userId, // Different user
397 commentId: comment.id,
398 });
399
400 expect(result).toBe(false);
401
402 // Verify comment was not deleted
403 const comments = await commentOps.getComments({ orgId, jobId: jobId1 });
404 expect(comments).toHaveLength(1);
405 },
406 );
407 });
408
409 describe('Cross-queue functionality integration', () => {
410 testWithFixtures(
411 'should show comments from previous queue after job moves',
412 async ({ commentOps, orgId, userId, jobId1, jobId2 }) => {
413 // Add comment to first job
414 await commentOps.addComment({
415 orgId,
416 jobId: jobId1,
417 commentText: 'Comment from original queue',
418 authorId: userId,
419 });
420
421 // Add comment to second job (simulating a move to a new queue)
422 await commentOps.addComment({
423 orgId,
424 jobId: jobId2,
425 commentText: 'Comment from new queue',
426 authorId: userId,
427 });
428
429 // When querying from the second job, should see both comments
430 const result = await commentOps.getComments({ orgId, jobId: jobId2 });
431
432 expect(result).toHaveLength(2);
433 const commentTexts = result.map((c) => c.commentText);
434 expect(commentTexts).toContain('Comment from original queue');
435 expect(commentTexts).toContain('Comment from new queue');
436 },
437 );
438
439 testWithFixtures(
440 'should count comments from all queues',
441 async ({ commentOps, orgId, userId, jobId1, jobId2 }) => {
442 // Add comments to both jobs
443 await commentOps.addComment({
444 orgId,
445 jobId: jobId1,
446 commentText: 'Comment 1',
447 authorId: userId,
448 });
449 await commentOps.addComment({
450 orgId,
451 jobId: jobId2,
452 commentText: 'Comment 2',
453 authorId: userId,
454 });
455 await commentOps.addComment({
456 orgId,
457 jobId: jobId2,
458 commentText: 'Comment 3',
459 authorId: userId,
460 });
461
462 const result = await commentOps.getCommentCount({ orgId, jobId: jobId2 });
463
464 expect(result).toBe(3);
465 },
466 );
467 });
468});