@flusys/nestjs-task-manager 6.0.2 → 6.1.0

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 (35) hide show
  1. package/cjs/controllers/task-board.controller.spec.js +132 -0
  2. package/cjs/controllers/task-comment.controller.spec.js +86 -0
  3. package/cjs/controllers/task-label.controller.spec.js +88 -0
  4. package/cjs/controllers/task-list.controller.spec.js +112 -0
  5. package/cjs/controllers/task.controller.spec.js +131 -0
  6. package/cjs/docs/task-manager-swagger.config.spec.js +28 -0
  7. package/cjs/entities/index.spec.js +30 -0
  8. package/cjs/gateways/task-board.gateway.spec.js +243 -0
  9. package/cjs/modules/task-manager.module.spec.js +144 -0
  10. package/cjs/services/task-activity.service.spec.js +125 -0
  11. package/cjs/services/task-board.service.spec.js +429 -0
  12. package/cjs/services/task-comment.service.spec.js +160 -0
  13. package/cjs/services/task-label.service.spec.js +138 -0
  14. package/cjs/services/task-list.service.spec.js +214 -0
  15. package/cjs/services/task-manager-config.service.spec.js +98 -0
  16. package/cjs/services/task-manager-datasource.provider.spec.js +131 -0
  17. package/cjs/services/task.service.spec.js +610 -0
  18. package/fesm/controllers/task-board.controller.spec.js +128 -0
  19. package/fesm/controllers/task-comment.controller.spec.js +82 -0
  20. package/fesm/controllers/task-label.controller.spec.js +84 -0
  21. package/fesm/controllers/task-list.controller.spec.js +108 -0
  22. package/fesm/controllers/task.controller.spec.js +127 -0
  23. package/fesm/docs/task-manager-swagger.config.spec.js +24 -0
  24. package/fesm/entities/index.spec.js +26 -0
  25. package/fesm/gateways/task-board.gateway.spec.js +239 -0
  26. package/fesm/modules/task-manager.module.spec.js +140 -0
  27. package/fesm/services/task-activity.service.spec.js +121 -0
  28. package/fesm/services/task-board.service.spec.js +425 -0
  29. package/fesm/services/task-comment.service.spec.js +156 -0
  30. package/fesm/services/task-label.service.spec.js +134 -0
  31. package/fesm/services/task-list.service.spec.js +210 -0
  32. package/fesm/services/task-manager-config.service.spec.js +94 -0
  33. package/fesm/services/task-manager-datasource.provider.spec.js +127 -0
  34. package/fesm/services/task.service.spec.js +606 -0
  35. package/package.json +3 -3
@@ -0,0 +1,610 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _testing = require("@nestjs/testing");
6
+ const _modules = require("@flusys/nestjs-shared/modules");
7
+ const _repositorymock = require("@test-utils/mocks/repository.mock");
8
+ const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
9
+ const _entities = require("../entities");
10
+ const _taskactivityservice = require("./task-activity.service");
11
+ const _taskboardservice = require("./task-board.service");
12
+ const _taskmanagerdatasourceprovider = require("./task-manager-datasource.provider");
13
+ const _taskservice = require("./task.service");
14
+ function buildTask(overrides = {}) {
15
+ return {
16
+ id: 'task-uuid-1',
17
+ boardId: 'board-uuid-1',
18
+ listId: 'list-uuid-1',
19
+ title: 'Fix bug',
20
+ description: null,
21
+ priority: _entities.TaskPriority.MEDIUM,
22
+ dueDate: null,
23
+ position: 0,
24
+ reporterId: null,
25
+ parentTaskId: null,
26
+ estimatedHours: null,
27
+ assignees: [],
28
+ createdAt: new Date(),
29
+ updatedAt: new Date(),
30
+ deletedAt: null,
31
+ createdById: null,
32
+ updatedById: null,
33
+ deletedById: null,
34
+ ...overrides
35
+ };
36
+ }
37
+ function mockQueryRunner(repo) {
38
+ const qr = {
39
+ connect: jest.fn().mockResolvedValue(undefined),
40
+ startTransaction: jest.fn().mockResolvedValue(undefined),
41
+ commitTransaction: jest.fn().mockResolvedValue(undefined),
42
+ rollbackTransaction: jest.fn().mockResolvedValue(undefined),
43
+ release: jest.fn().mockResolvedValue(undefined),
44
+ manager: {
45
+ save: jest.fn((_target, entities)=>Promise.resolve(entities))
46
+ }
47
+ };
48
+ repo.manager.connection.createQueryRunner.mockReturnValue(qr);
49
+ return qr;
50
+ }
51
+ describe('TaskService', ()=>{
52
+ let service;
53
+ let mockRepo;
54
+ let mockLabelJunctionRepo;
55
+ let mockFullLabelRepo;
56
+ let mockAssigneeRepo;
57
+ let mockBoardService;
58
+ let mockActivityService;
59
+ let mockDataSourceProvider;
60
+ beforeEach(async ()=>{
61
+ mockRepo = (0, _repositorymock.createMockRepository)();
62
+ mockLabelJunctionRepo = (0, _repositorymock.createMockRepository)();
63
+ mockFullLabelRepo = (0, _repositorymock.createMockRepository)();
64
+ mockAssigneeRepo = (0, _repositorymock.createMockRepository)();
65
+ mockLabelJunctionRepo.find.mockResolvedValue([]);
66
+ mockFullLabelRepo.find.mockResolvedValue([]);
67
+ mockAssigneeRepo.createQueryBuilder.mockReturnValue({
68
+ ...(0, _repositorymock.createMockQueryBuilder)(),
69
+ select: jest.fn().mockReturnThis(),
70
+ addSelect: jest.fn().mockReturnThis(),
71
+ leftJoin: jest.fn().mockReturnThis(),
72
+ where: jest.fn().mockReturnThis(),
73
+ getRawMany: jest.fn().mockResolvedValue([])
74
+ });
75
+ mockBoardService = {
76
+ isBoardMember: jest.fn().mockResolvedValue(true)
77
+ };
78
+ mockActivityService = {
79
+ log: jest.fn().mockResolvedValue(undefined)
80
+ };
81
+ mockDataSourceProvider = {
82
+ getRepository: jest.fn((entity)=>{
83
+ if (entity === _entities.TaskTaskLabel) return Promise.resolve(mockLabelJunctionRepo);
84
+ if (entity === _entities.TaskLabel) return Promise.resolve(mockFullLabelRepo);
85
+ if (entity === _entities.TaskAssignee) return Promise.resolve(mockAssigneeRepo);
86
+ return Promise.resolve(mockRepo);
87
+ }),
88
+ getDataSource: jest.fn()
89
+ };
90
+ const module = await _testing.Test.createTestingModule({
91
+ providers: [
92
+ _taskservice.TaskService,
93
+ {
94
+ provide: 'CACHE_INSTANCE',
95
+ useValue: {}
96
+ },
97
+ {
98
+ provide: _modules.UtilsService,
99
+ useValue: {
100
+ clearCache: jest.fn(),
101
+ getCacheKey: jest.fn(),
102
+ trackCacheKey: jest.fn()
103
+ }
104
+ },
105
+ {
106
+ provide: _taskmanagerdatasourceprovider.TaskManagerDataSourceProvider,
107
+ useValue: mockDataSourceProvider
108
+ },
109
+ {
110
+ provide: _taskboardservice.TaskBoardService,
111
+ useValue: mockBoardService
112
+ },
113
+ {
114
+ provide: _taskactivityservice.TaskActivityService,
115
+ useValue: mockActivityService
116
+ }
117
+ ]
118
+ }).compile();
119
+ service = await module.resolve(_taskservice.TaskService);
120
+ });
121
+ describe('convertSingleDtoToEntity / insert (create path)', ()=>{
122
+ it('creates a task with default priority, position 0 and reporterId from the current user', async ()=>{
123
+ mockQueryRunner(mockRepo);
124
+ const result = await service.insert({
125
+ boardId: 'board-1',
126
+ listId: 'list-1',
127
+ title: 'New Task'
128
+ }, (0, _loggedusermock.buildMockUser)({
129
+ id: 'reporter-1'
130
+ }));
131
+ expect(result).toEqual(expect.objectContaining({
132
+ boardId: 'board-1',
133
+ listId: 'list-1',
134
+ title: 'New Task',
135
+ priority: 'MEDIUM',
136
+ position: 0,
137
+ reporterId: 'reporter-1'
138
+ }));
139
+ });
140
+ it('respects an explicit priority and dueDate on create', async ()=>{
141
+ mockQueryRunner(mockRepo);
142
+ const result = await service.insert({
143
+ boardId: 'board-1',
144
+ listId: 'list-1',
145
+ title: 'Urgent Task',
146
+ priority: _entities.TaskPriority.URGENT,
147
+ dueDate: '2026-12-31'
148
+ }, (0, _loggedusermock.buildMockUser)());
149
+ expect(result).toEqual(expect.objectContaining({
150
+ priority: 'URGENT',
151
+ dueDate: '2026-12-31'
152
+ }));
153
+ });
154
+ it('validates every assignee is a board member and throws BadRequestException otherwise', async ()=>{
155
+ mockQueryRunner(mockRepo);
156
+ mockBoardService.isBoardMember.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
157
+ await expect(service.insert({
158
+ boardId: 'board-1',
159
+ listId: 'list-1',
160
+ title: 'Task',
161
+ assigneeIds: [
162
+ 'member-1',
163
+ 'stranger-1'
164
+ ]
165
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
166
+ });
167
+ it('allows creation when all assignees are board members', async ()=>{
168
+ mockQueryRunner(mockRepo);
169
+ mockBoardService.isBoardMember.mockResolvedValue(true);
170
+ await expect(service.insert({
171
+ boardId: 'board-1',
172
+ listId: 'list-1',
173
+ title: 'Task',
174
+ assigneeIds: [
175
+ 'member-1'
176
+ ]
177
+ }, (0, _loggedusermock.buildMockUser)())).resolves.toBeDefined();
178
+ expect(mockBoardService.isBoardMember).toHaveBeenCalledWith('board-1', 'member-1');
179
+ });
180
+ it('logs a "task.created" activity after a successful insert', async ()=>{
181
+ mockQueryRunner(mockRepo);
182
+ const result = await service.insert({
183
+ boardId: 'board-1',
184
+ listId: 'list-1',
185
+ title: 'New Task'
186
+ }, (0, _loggedusermock.buildMockUser)({
187
+ id: 'reporter-1'
188
+ }));
189
+ expect(mockActivityService.log).toHaveBeenCalledWith(result.id, 'board-1', 'reporter-1', 'task.created', null, {
190
+ title: 'New Task'
191
+ });
192
+ });
193
+ it('syncs labels via the junction repository on create when labelIds are provided', async ()=>{
194
+ mockQueryRunner(mockRepo);
195
+ mockLabelJunctionRepo.create.mockImplementation((data)=>data);
196
+ const result = await service.insert({
197
+ boardId: 'board-1',
198
+ listId: 'list-1',
199
+ title: 'Labeled Task',
200
+ labelIds: [
201
+ 'label-1',
202
+ 'label-2'
203
+ ]
204
+ }, (0, _loggedusermock.buildMockUser)());
205
+ expect(mockLabelJunctionRepo.delete).toHaveBeenCalledWith({
206
+ taskId: result.id
207
+ });
208
+ expect(mockLabelJunctionRepo.save).toHaveBeenCalledWith([
209
+ expect.objectContaining({
210
+ taskId: result.id,
211
+ labelId: 'label-1'
212
+ }),
213
+ expect.objectContaining({
214
+ taskId: result.id,
215
+ labelId: 'label-2'
216
+ })
217
+ ]);
218
+ });
219
+ it('syncs assignees via the assignee repository on create when assigneeIds are provided', async ()=>{
220
+ mockQueryRunner(mockRepo);
221
+ mockAssigneeRepo.create.mockImplementation((data)=>data);
222
+ const result = await service.insert({
223
+ boardId: 'board-1',
224
+ listId: 'list-1',
225
+ title: 'Assigned Task',
226
+ assigneeIds: [
227
+ 'user-1'
228
+ ]
229
+ }, (0, _loggedusermock.buildMockUser)());
230
+ expect(mockAssigneeRepo.save).toHaveBeenCalledWith([
231
+ expect.objectContaining({
232
+ taskId: result.id,
233
+ userId: 'user-1'
234
+ })
235
+ ]);
236
+ });
237
+ });
238
+ describe('convertSingleDtoToEntity / update (update path)', ()=>{
239
+ it('throws NotFoundException when updating a task that does not exist', async ()=>{
240
+ mockQueryRunner(mockRepo);
241
+ mockRepo.findOne.mockResolvedValue(null);
242
+ await expect(service.update({
243
+ id: 'missing',
244
+ title: 'Renamed'
245
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
246
+ });
247
+ it('merges update fields onto the existing entity, preserving position and reporterId', async ()=>{
248
+ mockQueryRunner(mockRepo);
249
+ const existing = buildTask({
250
+ id: 'task-1',
251
+ title: 'Old title',
252
+ position: 7,
253
+ reporterId: 'orig-reporter'
254
+ });
255
+ mockRepo.findOne.mockResolvedValue(existing);
256
+ const result = await service.update({
257
+ id: 'task-1',
258
+ title: 'New title'
259
+ }, (0, _loggedusermock.buildMockUser)());
260
+ expect(result).toEqual(expect.objectContaining({
261
+ id: 'task-1',
262
+ title: 'New title',
263
+ position: 7,
264
+ reporterId: 'orig-reporter'
265
+ }));
266
+ });
267
+ it('clears description when explicitly set to null on update', async ()=>{
268
+ mockQueryRunner(mockRepo);
269
+ const existing = buildTask({
270
+ id: 'task-1',
271
+ description: 'Old description'
272
+ });
273
+ mockRepo.findOne.mockResolvedValue(existing);
274
+ const result = await service.update({
275
+ id: 'task-1',
276
+ description: null
277
+ }, (0, _loggedusermock.buildMockUser)());
278
+ expect(result.description).toBeNull();
279
+ });
280
+ it('re-validates assignee board membership when assigneeIds change on update', async ()=>{
281
+ mockQueryRunner(mockRepo);
282
+ const existing = buildTask({
283
+ id: 'task-1'
284
+ });
285
+ mockRepo.findOne.mockResolvedValue(existing);
286
+ mockBoardService.isBoardMember.mockResolvedValue(false);
287
+ await expect(service.update({
288
+ id: 'task-1',
289
+ assigneeIds: [
290
+ 'not-a-member'
291
+ ]
292
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
293
+ });
294
+ it('only syncs labels/assignees on update when the respective dto fields are present', async ()=>{
295
+ mockQueryRunner(mockRepo);
296
+ const existing = buildTask({
297
+ id: 'task-1'
298
+ });
299
+ mockRepo.findOne.mockResolvedValue(existing);
300
+ await service.update({
301
+ id: 'task-1',
302
+ title: 'Just a rename'
303
+ }, (0, _loggedusermock.buildMockUser)());
304
+ expect(mockLabelJunctionRepo.delete).not.toHaveBeenCalled();
305
+ expect(mockAssigneeRepo.delete).not.toHaveBeenCalled();
306
+ });
307
+ });
308
+ describe('getFilterQuery', ()=>{
309
+ it('filters by boardId, listId and priority using andWhere', async ()=>{
310
+ const qb = (0, _repositorymock.createMockQueryBuilder)();
311
+ await service.getFilterQuery(qb, {
312
+ boardId: 'board-1',
313
+ listId: 'list-1',
314
+ priority: 'HIGH'
315
+ }, (0, _loggedusermock.buildMockUser)());
316
+ expect(qb.andWhere).toHaveBeenCalledWith('task.boardId = :boardId', {
317
+ boardId: 'board-1'
318
+ });
319
+ expect(qb.andWhere).toHaveBeenCalledWith('task.listId = :listId', {
320
+ listId: 'list-1'
321
+ });
322
+ expect(qb.andWhere).toHaveBeenCalledWith('task.priority = :priority', {
323
+ priority: 'HIGH'
324
+ });
325
+ });
326
+ it('joins the assignee table and filters by assigneeId', async ()=>{
327
+ const qb = (0, _repositorymock.createMockQueryBuilder)();
328
+ qb.innerJoin = jest.fn().mockReturnThis();
329
+ await service.getFilterQuery(qb, {
330
+ assigneeId: 'user-1'
331
+ }, (0, _loggedusermock.buildMockUser)());
332
+ expect(qb.innerJoin).toHaveBeenCalledWith('task_assignee', 'ta', 'ta.task_id = task.id');
333
+ expect(qb.andWhere).toHaveBeenCalledWith('ta.user_id = :assigneeUserId', {
334
+ assigneeUserId: 'user-1'
335
+ });
336
+ });
337
+ it('ignores unknown filter keys', async ()=>{
338
+ const qb = (0, _repositorymock.createMockQueryBuilder)();
339
+ await service.getFilterQuery(qb, {
340
+ unknownKey: 'value'
341
+ }, (0, _loggedusermock.buildMockUser)());
342
+ expect(qb.andWhere).not.toHaveBeenCalled();
343
+ });
344
+ });
345
+ describe('moveTask', ()=>{
346
+ it('moves a task to a target list and given position, logging "task.moved"', async ()=>{
347
+ const task = buildTask({
348
+ id: 'task-1',
349
+ listId: 'list-old',
350
+ boardId: 'board-1',
351
+ position: 0
352
+ });
353
+ mockRepo.findOne.mockResolvedValue(task);
354
+ mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
355
+ const result = await service.moveTask({
356
+ taskId: 'task-1',
357
+ targetListId: 'list-new',
358
+ position: 3
359
+ }, (0, _loggedusermock.buildMockUser)({
360
+ id: 'mover-1'
361
+ }));
362
+ expect(result.listId).toBe('list-new');
363
+ expect(result.position).toBe(3);
364
+ expect(result.updatedById).toBe('mover-1');
365
+ expect(mockActivityService.log).toHaveBeenCalledWith('task-1', 'board-1', 'mover-1', 'task.moved', {
366
+ listId: 'list-old'
367
+ }, {
368
+ listId: 'list-new'
369
+ });
370
+ });
371
+ it('defaults position to 0 (first position) when no position is given', async ()=>{
372
+ const task = buildTask({
373
+ id: 'task-1',
374
+ listId: 'list-old'
375
+ });
376
+ mockRepo.findOne.mockResolvedValue(task);
377
+ mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
378
+ const result = await service.moveTask({
379
+ taskId: 'task-1',
380
+ targetListId: 'list-new'
381
+ }, (0, _loggedusermock.buildMockUser)());
382
+ expect(result.position).toBe(0);
383
+ });
384
+ it('supports moving to the last position of a target list via an explicit high position value', async ()=>{
385
+ const task = buildTask({
386
+ id: 'task-1',
387
+ listId: 'list-old'
388
+ });
389
+ mockRepo.findOne.mockResolvedValue(task);
390
+ mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
391
+ const result = await service.moveTask({
392
+ taskId: 'task-1',
393
+ targetListId: 'list-new',
394
+ position: 99
395
+ }, (0, _loggedusermock.buildMockUser)());
396
+ expect(result.position).toBe(99);
397
+ });
398
+ it('supports moving within the same list (reordering, not a cross-list move)', async ()=>{
399
+ const task = buildTask({
400
+ id: 'task-1',
401
+ listId: 'list-same',
402
+ boardId: 'board-1',
403
+ position: 0
404
+ });
405
+ mockRepo.findOne.mockResolvedValue(task);
406
+ mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
407
+ const result = await service.moveTask({
408
+ taskId: 'task-1',
409
+ targetListId: 'list-same',
410
+ position: 2
411
+ }, (0, _loggedusermock.buildMockUser)());
412
+ expect(result.listId).toBe('list-same');
413
+ expect(result.position).toBe(2);
414
+ expect(mockActivityService.log).toHaveBeenCalledWith('task-1', 'board-1', expect.anything(), 'task.moved', {
415
+ listId: 'list-same'
416
+ }, {
417
+ listId: 'list-same'
418
+ });
419
+ });
420
+ it('throws NotFoundException when the task to move does not exist', async ()=>{
421
+ mockRepo.findOne.mockResolvedValue(null);
422
+ await expect(service.moveTask({
423
+ taskId: 'missing',
424
+ targetListId: 'list-new'
425
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
426
+ });
427
+ it('excludes soft-deleted tasks from being moved', async ()=>{
428
+ mockRepo.findOne.mockResolvedValue(null);
429
+ await expect(service.moveTask({
430
+ taskId: 'deleted-task',
431
+ targetListId: 'list-new'
432
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
433
+ expect(mockRepo.findOne).toHaveBeenCalledWith({
434
+ where: {
435
+ id: 'deleted-task',
436
+ deletedAt: expect.anything()
437
+ }
438
+ });
439
+ });
440
+ });
441
+ describe('assignTask', ()=>{
442
+ it('assigns board members to a task and logs "task.assigned" with old/new assignee ids', async ()=>{
443
+ const task = buildTask({
444
+ id: 'task-1',
445
+ boardId: 'board-1'
446
+ });
447
+ mockRepo.findOne.mockResolvedValue(task);
448
+ mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
449
+ mockAssigneeRepo.find.mockResolvedValue([
450
+ {
451
+ userId: 'old-user'
452
+ }
453
+ ]);
454
+ mockAssigneeRepo.create.mockImplementation((data)=>data);
455
+ mockBoardService.isBoardMember.mockResolvedValue(true);
456
+ const result = await service.assignTask({
457
+ taskId: 'task-1',
458
+ assigneeIds: [
459
+ 'new-user-1',
460
+ 'new-user-2'
461
+ ]
462
+ }, (0, _loggedusermock.buildMockUser)({
463
+ id: 'assigner-1'
464
+ }));
465
+ expect(mockAssigneeRepo.delete).toHaveBeenCalledWith({
466
+ taskId: 'task-1'
467
+ });
468
+ expect(mockAssigneeRepo.save).toHaveBeenCalledWith([
469
+ expect.objectContaining({
470
+ taskId: 'task-1',
471
+ userId: 'new-user-1'
472
+ }),
473
+ expect.objectContaining({
474
+ taskId: 'task-1',
475
+ userId: 'new-user-2'
476
+ })
477
+ ]);
478
+ expect(mockActivityService.log).toHaveBeenCalledWith('task-1', 'board-1', 'assigner-1', 'task.assigned', {
479
+ assigneeIds: [
480
+ 'old-user'
481
+ ]
482
+ }, {
483
+ assigneeIds: [
484
+ 'new-user-1',
485
+ 'new-user-2'
486
+ ]
487
+ });
488
+ expect(result.updatedById).toBe('assigner-1');
489
+ });
490
+ it('supports unassigning all users by passing an empty assigneeIds array', async ()=>{
491
+ const task = buildTask({
492
+ id: 'task-1',
493
+ boardId: 'board-1'
494
+ });
495
+ mockRepo.findOne.mockResolvedValue(task);
496
+ mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
497
+ mockAssigneeRepo.find.mockResolvedValue([
498
+ {
499
+ userId: 'old-user'
500
+ }
501
+ ]);
502
+ await service.assignTask({
503
+ taskId: 'task-1',
504
+ assigneeIds: []
505
+ }, (0, _loggedusermock.buildMockUser)());
506
+ expect(mockAssigneeRepo.delete).toHaveBeenCalledWith({
507
+ taskId: 'task-1'
508
+ });
509
+ expect(mockAssigneeRepo.save).not.toHaveBeenCalled();
510
+ });
511
+ it('throws BadRequestException when any assignee is not a board member', async ()=>{
512
+ const task = buildTask({
513
+ id: 'task-1',
514
+ boardId: 'board-1'
515
+ });
516
+ mockRepo.findOne.mockResolvedValue(task);
517
+ mockBoardService.isBoardMember.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
518
+ await expect(service.assignTask({
519
+ taskId: 'task-1',
520
+ assigneeIds: [
521
+ 'member-1',
522
+ 'stranger-1'
523
+ ]
524
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
525
+ expect(mockAssigneeRepo.delete).not.toHaveBeenCalled();
526
+ });
527
+ it('throws NotFoundException when the task does not exist', async ()=>{
528
+ mockRepo.findOne.mockResolvedValue(null);
529
+ await expect(service.assignTask({
530
+ taskId: 'missing',
531
+ assigneeIds: [
532
+ 'user-1'
533
+ ]
534
+ }, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
535
+ });
536
+ it('returns the task with freshly loaded assignees populated', async ()=>{
537
+ const task = buildTask({
538
+ id: 'task-1',
539
+ boardId: 'board-1'
540
+ });
541
+ mockRepo.findOne.mockResolvedValue(task);
542
+ mockRepo.save.mockImplementation((t)=>Promise.resolve(t));
543
+ mockAssigneeRepo.find.mockResolvedValue([]);
544
+ const rawRows = [
545
+ {
546
+ userId: 'new-user-1',
547
+ userName: 'New User'
548
+ }
549
+ ];
550
+ mockAssigneeRepo.createQueryBuilder.mockReturnValue({
551
+ select: jest.fn().mockReturnThis(),
552
+ addSelect: jest.fn().mockReturnThis(),
553
+ leftJoin: jest.fn().mockReturnThis(),
554
+ where: jest.fn().mockReturnThis(),
555
+ getRawMany: jest.fn().mockResolvedValue(rawRows)
556
+ });
557
+ const result = await service.assignTask({
558
+ taskId: 'task-1',
559
+ assigneeIds: [
560
+ 'new-user-1'
561
+ ]
562
+ }, (0, _loggedusermock.buildMockUser)());
563
+ expect(result.assignees).toEqual([
564
+ {
565
+ userId: 'new-user-1',
566
+ userName: 'New User'
567
+ }
568
+ ]);
569
+ });
570
+ });
571
+ describe('getAll', ()=>{
572
+ it('loads labels and assignees for every returned task', async ()=>{
573
+ const tasks = [
574
+ buildTask({
575
+ id: 'task-1'
576
+ }),
577
+ buildTask({
578
+ id: 'task-2'
579
+ })
580
+ ];
581
+ const superGetAllSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(service)), 'getAll').mockResolvedValue({
582
+ data: tasks,
583
+ total: 2
584
+ });
585
+ mockLabelJunctionRepo.find.mockResolvedValue([]);
586
+ mockAssigneeRepo.createQueryBuilder.mockReturnValue({
587
+ select: jest.fn().mockReturnThis(),
588
+ addSelect: jest.fn().mockReturnThis(),
589
+ leftJoin: jest.fn().mockReturnThis(),
590
+ where: jest.fn().mockReturnThis(),
591
+ getRawMany: jest.fn().mockResolvedValue([])
592
+ });
593
+ const result = await service.getAll('', {}, (0, _loggedusermock.buildMockUser)());
594
+ expect(result.data).toHaveLength(2);
595
+ expect(mockLabelJunctionRepo.find).toHaveBeenCalled();
596
+ expect(mockAssigneeRepo.createQueryBuilder).toHaveBeenCalled();
597
+ superGetAllSpy.mockRestore();
598
+ });
599
+ it('returns an empty result set without querying label/assignee tables when there are no tasks', async ()=>{
600
+ const superGetAllSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(service)), 'getAll').mockResolvedValue({
601
+ data: [],
602
+ total: 0
603
+ });
604
+ const result = await service.getAll('', {}, (0, _loggedusermock.buildMockUser)());
605
+ expect(result.data).toEqual([]);
606
+ expect(mockLabelJunctionRepo.find).not.toHaveBeenCalled();
607
+ superGetAllSpy.mockRestore();
608
+ });
609
+ });
610
+ });