@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.
- package/cjs/controllers/task-board.controller.spec.js +132 -0
- package/cjs/controllers/task-comment.controller.spec.js +86 -0
- package/cjs/controllers/task-label.controller.spec.js +88 -0
- package/cjs/controllers/task-list.controller.spec.js +112 -0
- package/cjs/controllers/task.controller.spec.js +131 -0
- package/cjs/docs/task-manager-swagger.config.spec.js +28 -0
- package/cjs/entities/index.spec.js +30 -0
- package/cjs/gateways/task-board.gateway.spec.js +243 -0
- package/cjs/modules/task-manager.module.spec.js +144 -0
- package/cjs/services/task-activity.service.spec.js +125 -0
- package/cjs/services/task-board.service.spec.js +429 -0
- package/cjs/services/task-comment.service.spec.js +160 -0
- package/cjs/services/task-label.service.spec.js +138 -0
- package/cjs/services/task-list.service.spec.js +214 -0
- package/cjs/services/task-manager-config.service.spec.js +98 -0
- package/cjs/services/task-manager-datasource.provider.spec.js +131 -0
- package/cjs/services/task.service.spec.js +610 -0
- package/fesm/controllers/task-board.controller.spec.js +128 -0
- package/fesm/controllers/task-comment.controller.spec.js +82 -0
- package/fesm/controllers/task-label.controller.spec.js +84 -0
- package/fesm/controllers/task-list.controller.spec.js +108 -0
- package/fesm/controllers/task.controller.spec.js +127 -0
- package/fesm/docs/task-manager-swagger.config.spec.js +24 -0
- package/fesm/entities/index.spec.js +26 -0
- package/fesm/gateways/task-board.gateway.spec.js +239 -0
- package/fesm/modules/task-manager.module.spec.js +140 -0
- package/fesm/services/task-activity.service.spec.js +121 -0
- package/fesm/services/task-board.service.spec.js +425 -0
- package/fesm/services/task-comment.service.spec.js +156 -0
- package/fesm/services/task-label.service.spec.js +134 -0
- package/fesm/services/task-list.service.spec.js +210 -0
- package/fesm/services/task-manager-config.service.spec.js +94 -0
- package/fesm/services/task-manager-datasource.provider.spec.js +127 -0
- package/fesm/services/task.service.spec.js +606 -0
- package/package.json +3 -3
|
@@ -0,0 +1,429 @@
|
|
|
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 _taskboardservice = require("./task-board.service");
|
|
11
|
+
const _taskmanagerconfigservice = require("./task-manager-config.service");
|
|
12
|
+
const _taskmanagerdatasourceprovider = require("./task-manager-datasource.provider");
|
|
13
|
+
function buildBoard(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
id: 'board-uuid-1',
|
|
16
|
+
name: 'Sprint Board',
|
|
17
|
+
description: null,
|
|
18
|
+
color: '#3B82F6',
|
|
19
|
+
isActive: true,
|
|
20
|
+
settings: null,
|
|
21
|
+
createdAt: new Date(),
|
|
22
|
+
updatedAt: new Date(),
|
|
23
|
+
deletedAt: null,
|
|
24
|
+
createdById: null,
|
|
25
|
+
updatedById: null,
|
|
26
|
+
deletedById: null,
|
|
27
|
+
...overrides
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function buildMember(overrides = {}) {
|
|
31
|
+
return {
|
|
32
|
+
id: 'member-uuid-1',
|
|
33
|
+
boardId: 'board-uuid-1',
|
|
34
|
+
userId: 'user-uuid-1',
|
|
35
|
+
role: _entities.BoardMemberRole.MEMBER,
|
|
36
|
+
createdAt: new Date(),
|
|
37
|
+
updatedAt: new Date(),
|
|
38
|
+
deletedAt: null,
|
|
39
|
+
createdById: null,
|
|
40
|
+
updatedById: null,
|
|
41
|
+
deletedById: null,
|
|
42
|
+
...overrides
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function mockQueryRunner(repo) {
|
|
46
|
+
const qr = {
|
|
47
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
48
|
+
startTransaction: jest.fn().mockResolvedValue(undefined),
|
|
49
|
+
commitTransaction: jest.fn().mockResolvedValue(undefined),
|
|
50
|
+
rollbackTransaction: jest.fn().mockResolvedValue(undefined),
|
|
51
|
+
release: jest.fn().mockResolvedValue(undefined),
|
|
52
|
+
manager: {
|
|
53
|
+
save: jest.fn((_target, entities)=>Promise.resolve(entities))
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
repo.manager.connection.createQueryRunner.mockReturnValue(qr);
|
|
57
|
+
return qr;
|
|
58
|
+
}
|
|
59
|
+
describe('TaskBoardService', ()=>{
|
|
60
|
+
let service;
|
|
61
|
+
let mockRepo;
|
|
62
|
+
let mockMemberRepo;
|
|
63
|
+
let mockConfig;
|
|
64
|
+
let mockDataSourceProvider;
|
|
65
|
+
beforeEach(async ()=>{
|
|
66
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
67
|
+
mockMemberRepo = (0, _repositorymock.createMockRepository)();
|
|
68
|
+
mockConfig = {
|
|
69
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
|
|
70
|
+
};
|
|
71
|
+
mockDataSourceProvider = {
|
|
72
|
+
getRepository: jest.fn((entity)=>{
|
|
73
|
+
if (entity === _entities.TaskBoardMember) return Promise.resolve(mockMemberRepo);
|
|
74
|
+
return Promise.resolve(mockRepo);
|
|
75
|
+
}),
|
|
76
|
+
getDataSource: jest.fn()
|
|
77
|
+
};
|
|
78
|
+
const module = await _testing.Test.createTestingModule({
|
|
79
|
+
providers: [
|
|
80
|
+
_taskboardservice.TaskBoardService,
|
|
81
|
+
{
|
|
82
|
+
provide: 'CACHE_INSTANCE',
|
|
83
|
+
useValue: {}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
provide: _modules.UtilsService,
|
|
87
|
+
useValue: {
|
|
88
|
+
clearCache: jest.fn(),
|
|
89
|
+
getCacheKey: jest.fn(),
|
|
90
|
+
trackCacheKey: jest.fn()
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
provide: _taskmanagerconfigservice.TaskManagerConfigService,
|
|
95
|
+
useValue: mockConfig
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
provide: _taskmanagerdatasourceprovider.TaskManagerDataSourceProvider,
|
|
99
|
+
useValue: mockDataSourceProvider
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}).compile();
|
|
103
|
+
service = await module.resolve(_taskboardservice.TaskBoardService);
|
|
104
|
+
});
|
|
105
|
+
describe('resolveEntity', ()=>{
|
|
106
|
+
it('resolves to plain TaskBoard when company feature is disabled', ()=>{
|
|
107
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(false);
|
|
108
|
+
expect(service.resolveEntity()).toBe(_entities.TaskBoard);
|
|
109
|
+
});
|
|
110
|
+
it('resolves to TaskBoardWithCompany when company feature is enabled', ()=>{
|
|
111
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
112
|
+
expect(service.resolveEntity()).toBe(_entities.TaskBoardWithCompany);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('convertSingleDtoToEntity (via insert/update)', ()=>{
|
|
116
|
+
it('creates a board applying default color and active flag', async ()=>{
|
|
117
|
+
mockQueryRunner(mockRepo);
|
|
118
|
+
const result = await service.insert({
|
|
119
|
+
name: 'New Board'
|
|
120
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
121
|
+
expect(result).toEqual(expect.objectContaining({
|
|
122
|
+
name: 'New Board',
|
|
123
|
+
color: '#3B82F6',
|
|
124
|
+
isActive: true
|
|
125
|
+
}));
|
|
126
|
+
expect(result.companyId).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
it('stamps companyId from the user when the company feature is enabled', async ()=>{
|
|
129
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
130
|
+
mockQueryRunner(mockRepo);
|
|
131
|
+
const result = await service.insert({
|
|
132
|
+
name: 'Company Board'
|
|
133
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
134
|
+
companyId: 'company-9'
|
|
135
|
+
}));
|
|
136
|
+
expect(result.companyId).toBe('company-9');
|
|
137
|
+
});
|
|
138
|
+
it('throws NotFoundException when updating a non-existent board', async ()=>{
|
|
139
|
+
mockQueryRunner(mockRepo);
|
|
140
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
141
|
+
await expect(service.update({
|
|
142
|
+
id: 'missing',
|
|
143
|
+
name: 'Renamed'
|
|
144
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
|
|
145
|
+
});
|
|
146
|
+
it('merges update fields, preserving description as null when explicitly cleared', async ()=>{
|
|
147
|
+
mockQueryRunner(mockRepo);
|
|
148
|
+
const existing = buildBoard({
|
|
149
|
+
id: 'board-1',
|
|
150
|
+
description: 'Old description'
|
|
151
|
+
});
|
|
152
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
153
|
+
const result = await service.update({
|
|
154
|
+
id: 'board-1',
|
|
155
|
+
description: null
|
|
156
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
157
|
+
expect(result.description).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('afterInsertOperation', ()=>{
|
|
161
|
+
it('adds the creating user as an OWNER member of every newly created board', async ()=>{
|
|
162
|
+
mockQueryRunner(mockRepo);
|
|
163
|
+
mockMemberRepo.create.mockImplementation((data)=>data);
|
|
164
|
+
mockMemberRepo.save.mockResolvedValue([]);
|
|
165
|
+
await service.insert({
|
|
166
|
+
name: 'New Board'
|
|
167
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
168
|
+
id: 'creator-1'
|
|
169
|
+
}));
|
|
170
|
+
expect(mockMemberRepo.save).toHaveBeenCalledWith([
|
|
171
|
+
expect.objectContaining({
|
|
172
|
+
userId: 'creator-1',
|
|
173
|
+
role: _entities.BoardMemberRole.OWNER,
|
|
174
|
+
createdById: 'creator-1'
|
|
175
|
+
})
|
|
176
|
+
]);
|
|
177
|
+
});
|
|
178
|
+
it('does nothing when no user is present (system-created board)', async ()=>{
|
|
179
|
+
mockQueryRunner(mockRepo);
|
|
180
|
+
await service.insert({
|
|
181
|
+
name: 'System Board'
|
|
182
|
+
}, null);
|
|
183
|
+
expect(mockMemberRepo.save).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('getExtraManipulateQuery', ()=>{
|
|
187
|
+
it('applies the company filter, current-user membership join, and aggregate subselects', async ()=>{
|
|
188
|
+
const qb = (0, _repositorymock.createMockQueryBuilder)();
|
|
189
|
+
qb.innerJoin = jest.fn().mockReturnThis();
|
|
190
|
+
qb.addSelect = jest.fn().mockReturnThis();
|
|
191
|
+
mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
192
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
193
|
+
id: 'user-1',
|
|
194
|
+
companyId: 'company-1'
|
|
195
|
+
});
|
|
196
|
+
const { isRaw } = await service.getExtraManipulateQuery(qb, {}, user);
|
|
197
|
+
expect(qb.andWhere).toHaveBeenCalledWith('task_board.companyId = :companyId', {
|
|
198
|
+
companyId: 'company-1'
|
|
199
|
+
});
|
|
200
|
+
expect(qb.innerJoin).toHaveBeenCalledWith('task_board_member', 'tbm_filter', expect.stringContaining('tbm_filter.user_id = :currentUserId'), {
|
|
201
|
+
currentUserId: 'user-1'
|
|
202
|
+
});
|
|
203
|
+
expect(qb.addSelect).toHaveBeenCalledWith(expect.stringContaining('task_board_member'), 'total_members');
|
|
204
|
+
expect(qb.addSelect).toHaveBeenCalledWith(expect.stringContaining('task_list'), 'total_lists');
|
|
205
|
+
expect(qb.addSelect).toHaveBeenCalledWith(expect.stringContaining('FROM task t'), 'total_tasks');
|
|
206
|
+
expect(isRaw).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
it('skips the membership join when no user is provided', async ()=>{
|
|
209
|
+
const qb = (0, _repositorymock.createMockQueryBuilder)();
|
|
210
|
+
qb.innerJoin = jest.fn().mockReturnThis();
|
|
211
|
+
qb.addSelect = jest.fn().mockReturnThis();
|
|
212
|
+
await service.getExtraManipulateQuery(qb, {}, null);
|
|
213
|
+
expect(qb.innerJoin).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
describe('convertEntityToResponseDto', ()=>{
|
|
217
|
+
it('passes through non-raw entities unchanged (default behavior)', ()=>{
|
|
218
|
+
const board = buildBoard();
|
|
219
|
+
const result = service.convertEntityToResponseDto(board, false);
|
|
220
|
+
expect(result).toEqual(board);
|
|
221
|
+
});
|
|
222
|
+
it('strips the entity-name prefix and camelCases raw aggregate columns, converting numeric strings', ()=>{
|
|
223
|
+
const raw = {
|
|
224
|
+
task_board_id: 'board-1',
|
|
225
|
+
task_board_name: 'Sprint Board',
|
|
226
|
+
total_members: '3',
|
|
227
|
+
total_lists: '2'
|
|
228
|
+
};
|
|
229
|
+
const result = service.convertEntityToResponseDto(raw, true);
|
|
230
|
+
expect(result).toEqual({
|
|
231
|
+
id: 'board-1',
|
|
232
|
+
name: 'Sprint Board',
|
|
233
|
+
totalMembers: 3,
|
|
234
|
+
totalLists: 2
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
describe('addMember', ()=>{
|
|
239
|
+
it('adds a new member with the requested role', async ()=>{
|
|
240
|
+
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
241
|
+
id: 'board-1'
|
|
242
|
+
}));
|
|
243
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(null); // no active existing member
|
|
244
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(null); // no soft-deleted member either
|
|
245
|
+
mockMemberRepo.create.mockImplementation((data)=>data);
|
|
246
|
+
const created = buildMember({
|
|
247
|
+
boardId: 'board-1',
|
|
248
|
+
userId: 'user-2',
|
|
249
|
+
role: _entities.BoardMemberRole.ADMIN
|
|
250
|
+
});
|
|
251
|
+
mockMemberRepo.save.mockResolvedValue(created);
|
|
252
|
+
const result = await service.addMember({
|
|
253
|
+
boardId: 'board-1',
|
|
254
|
+
userId: 'user-2',
|
|
255
|
+
role: _entities.BoardMemberRole.ADMIN
|
|
256
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
257
|
+
id: 'owner-1'
|
|
258
|
+
}));
|
|
259
|
+
expect(mockMemberRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
260
|
+
boardId: 'board-1',
|
|
261
|
+
userId: 'user-2',
|
|
262
|
+
role: _entities.BoardMemberRole.ADMIN,
|
|
263
|
+
createdById: 'owner-1'
|
|
264
|
+
}));
|
|
265
|
+
expect(result).toEqual(created);
|
|
266
|
+
});
|
|
267
|
+
it('defaults the role to MEMBER when none is given', async ()=>{
|
|
268
|
+
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
269
|
+
id: 'board-1'
|
|
270
|
+
}));
|
|
271
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(null);
|
|
272
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(null);
|
|
273
|
+
mockMemberRepo.create.mockImplementation((data)=>data);
|
|
274
|
+
mockMemberRepo.save.mockResolvedValue(buildMember());
|
|
275
|
+
await service.addMember({
|
|
276
|
+
boardId: 'board-1',
|
|
277
|
+
userId: 'user-2'
|
|
278
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
279
|
+
expect(mockMemberRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
280
|
+
role: _entities.BoardMemberRole.MEMBER
|
|
281
|
+
}));
|
|
282
|
+
});
|
|
283
|
+
it('throws NotFoundException when the board does not exist', async ()=>{
|
|
284
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
285
|
+
await expect(service.addMember({
|
|
286
|
+
boardId: 'missing-board',
|
|
287
|
+
userId: 'user-2'
|
|
288
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
|
|
289
|
+
});
|
|
290
|
+
it('throws BadRequestException when the user is already an active member', async ()=>{
|
|
291
|
+
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
292
|
+
id: 'board-1'
|
|
293
|
+
}));
|
|
294
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(buildMember());
|
|
295
|
+
await expect(service.addMember({
|
|
296
|
+
boardId: 'board-1',
|
|
297
|
+
userId: 'user-uuid-1'
|
|
298
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
|
|
299
|
+
});
|
|
300
|
+
it('restores a soft-deleted membership instead of creating a duplicate row', async ()=>{
|
|
301
|
+
mockRepo.findOne.mockResolvedValue(buildBoard({
|
|
302
|
+
id: 'board-1'
|
|
303
|
+
}));
|
|
304
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(null); // no active member
|
|
305
|
+
const softDeleted = buildMember({
|
|
306
|
+
id: 'member-old',
|
|
307
|
+
deletedAt: new Date()
|
|
308
|
+
});
|
|
309
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(softDeleted); // soft-deleted row found
|
|
310
|
+
mockMemberRepo.restore.mockResolvedValue(undefined);
|
|
311
|
+
mockMemberRepo.update.mockResolvedValue(undefined);
|
|
312
|
+
const restored = buildMember({
|
|
313
|
+
id: 'member-old',
|
|
314
|
+
role: _entities.BoardMemberRole.ADMIN
|
|
315
|
+
});
|
|
316
|
+
mockMemberRepo.findOne.mockResolvedValueOnce(restored); // re-fetch after restore
|
|
317
|
+
const result = await service.addMember({
|
|
318
|
+
boardId: 'board-1',
|
|
319
|
+
userId: 'user-uuid-1',
|
|
320
|
+
role: _entities.BoardMemberRole.ADMIN
|
|
321
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
322
|
+
expect(mockMemberRepo.restore).toHaveBeenCalledWith({
|
|
323
|
+
id: 'member-old'
|
|
324
|
+
});
|
|
325
|
+
expect(mockMemberRepo.update).toHaveBeenCalledWith({
|
|
326
|
+
id: 'member-old'
|
|
327
|
+
}, {
|
|
328
|
+
role: _entities.BoardMemberRole.ADMIN
|
|
329
|
+
});
|
|
330
|
+
expect(mockMemberRepo.create).not.toHaveBeenCalled();
|
|
331
|
+
expect(result).toEqual(restored);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
describe('removeMember', ()=>{
|
|
335
|
+
it('soft-deletes an existing member', async ()=>{
|
|
336
|
+
const member = buildMember({
|
|
337
|
+
id: 'member-1'
|
|
338
|
+
});
|
|
339
|
+
mockMemberRepo.findOne.mockResolvedValue(member);
|
|
340
|
+
await service.removeMember({
|
|
341
|
+
boardId: 'board-1',
|
|
342
|
+
userId: 'user-uuid-1'
|
|
343
|
+
});
|
|
344
|
+
expect(mockMemberRepo.update).toHaveBeenCalledWith({
|
|
345
|
+
id: 'member-1'
|
|
346
|
+
}, {
|
|
347
|
+
deletedAt: expect.any(Date)
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
it('throws NotFoundException when the member is not found', async ()=>{
|
|
351
|
+
mockMemberRepo.findOne.mockResolvedValue(null);
|
|
352
|
+
await expect(service.removeMember({
|
|
353
|
+
boardId: 'board-1',
|
|
354
|
+
userId: 'unknown-user'
|
|
355
|
+
})).rejects.toThrow();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
function buildMembersQueryBuilder() {
|
|
359
|
+
const qb = (0, _repositorymock.createMockQueryBuilder)();
|
|
360
|
+
qb.leftJoin = jest.fn().mockReturnThis();
|
|
361
|
+
qb.addSelect = jest.fn().mockReturnThis();
|
|
362
|
+
qb.getRawAndEntities = jest.fn();
|
|
363
|
+
return qb;
|
|
364
|
+
}
|
|
365
|
+
describe('getMembers', ()=>{
|
|
366
|
+
it('returns members enriched with joined user name/email', async ()=>{
|
|
367
|
+
const qb = buildMembersQueryBuilder();
|
|
368
|
+
qb.getRawAndEntities.mockResolvedValue({
|
|
369
|
+
entities: [
|
|
370
|
+
buildMember({
|
|
371
|
+
id: 'member-1',
|
|
372
|
+
userId: 'user-1'
|
|
373
|
+
})
|
|
374
|
+
],
|
|
375
|
+
raw: [
|
|
376
|
+
{
|
|
377
|
+
userName: 'Alice',
|
|
378
|
+
userEmail: 'alice@example.com'
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
});
|
|
382
|
+
mockMemberRepo.createQueryBuilder.mockReturnValue(qb);
|
|
383
|
+
const result = await service.getMembers('board-1');
|
|
384
|
+
expect(result).toEqual([
|
|
385
|
+
expect.objectContaining({
|
|
386
|
+
id: 'member-1',
|
|
387
|
+
userName: 'Alice',
|
|
388
|
+
userEmail: 'alice@example.com'
|
|
389
|
+
})
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
it('defaults userName/userEmail to null when the joined user row is missing', async ()=>{
|
|
393
|
+
const qb = buildMembersQueryBuilder();
|
|
394
|
+
qb.getRawAndEntities.mockResolvedValue({
|
|
395
|
+
entities: [
|
|
396
|
+
buildMember({
|
|
397
|
+
id: 'member-1'
|
|
398
|
+
})
|
|
399
|
+
],
|
|
400
|
+
raw: [
|
|
401
|
+
{}
|
|
402
|
+
]
|
|
403
|
+
});
|
|
404
|
+
mockMemberRepo.createQueryBuilder.mockReturnValue(qb);
|
|
405
|
+
const result = await service.getMembers('board-1');
|
|
406
|
+
expect(result[0].userName).toBeNull();
|
|
407
|
+
expect(result[0].userEmail).toBeNull();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
describe('isBoardMember', ()=>{
|
|
411
|
+
it('returns true when an active membership row exists', async ()=>{
|
|
412
|
+
mockMemberRepo.count.mockResolvedValue(1);
|
|
413
|
+
const result = await service.isBoardMember('board-1', 'user-1');
|
|
414
|
+
expect(result).toBe(true);
|
|
415
|
+
expect(mockMemberRepo.count).toHaveBeenCalledWith({
|
|
416
|
+
where: {
|
|
417
|
+
boardId: 'board-1',
|
|
418
|
+
userId: 'user-1',
|
|
419
|
+
deletedAt: expect.anything()
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
it('returns false when no membership row exists', async ()=>{
|
|
424
|
+
mockMemberRepo.count.mockResolvedValue(0);
|
|
425
|
+
const result = await service.isBoardMember('board-1', 'stranger');
|
|
426
|
+
expect(result).toBe(false);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
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 _taskcommentservice = require("./task-comment.service");
|
|
10
|
+
const _taskmanagerdatasourceprovider = require("./task-manager-datasource.provider");
|
|
11
|
+
function buildTaskComment(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
id: 'comment-uuid-1',
|
|
14
|
+
taskId: 'task-uuid-1',
|
|
15
|
+
userId: 'user-uuid-1',
|
|
16
|
+
content: 'Original comment',
|
|
17
|
+
parentCommentId: null,
|
|
18
|
+
createdAt: new Date(),
|
|
19
|
+
updatedAt: new Date(),
|
|
20
|
+
deletedAt: null,
|
|
21
|
+
createdById: null,
|
|
22
|
+
updatedById: null,
|
|
23
|
+
deletedById: null,
|
|
24
|
+
...overrides
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function mockQueryRunner(repo) {
|
|
28
|
+
const qr = {
|
|
29
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
30
|
+
startTransaction: jest.fn().mockResolvedValue(undefined),
|
|
31
|
+
commitTransaction: jest.fn().mockResolvedValue(undefined),
|
|
32
|
+
rollbackTransaction: jest.fn().mockResolvedValue(undefined),
|
|
33
|
+
release: jest.fn().mockResolvedValue(undefined),
|
|
34
|
+
manager: {
|
|
35
|
+
save: jest.fn((_target, entities)=>Promise.resolve(entities))
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
repo.manager.connection.createQueryRunner.mockReturnValue(qr);
|
|
39
|
+
return qr;
|
|
40
|
+
}
|
|
41
|
+
describe('TaskCommentService', ()=>{
|
|
42
|
+
let service;
|
|
43
|
+
let mockRepo;
|
|
44
|
+
beforeEach(async ()=>{
|
|
45
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
46
|
+
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
47
|
+
const module = await _testing.Test.createTestingModule({
|
|
48
|
+
providers: [
|
|
49
|
+
_taskcommentservice.TaskCommentService,
|
|
50
|
+
{
|
|
51
|
+
provide: 'CACHE_INSTANCE',
|
|
52
|
+
useValue: {}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
provide: _modules.UtilsService,
|
|
56
|
+
useValue: {
|
|
57
|
+
clearCache: jest.fn(),
|
|
58
|
+
getCacheKey: jest.fn(),
|
|
59
|
+
trackCacheKey: jest.fn()
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
provide: _taskmanagerdatasourceprovider.TaskManagerDataSourceProvider,
|
|
64
|
+
useValue: mockDataSourceProvider
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}).compile();
|
|
68
|
+
service = await module.resolve(_taskcommentservice.TaskCommentService);
|
|
69
|
+
});
|
|
70
|
+
describe('convertSingleDtoToEntity (via insert)', ()=>{
|
|
71
|
+
it('creates a top-level comment authored by the current user', async ()=>{
|
|
72
|
+
mockQueryRunner(mockRepo);
|
|
73
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
74
|
+
id: 'author-1'
|
|
75
|
+
});
|
|
76
|
+
const result = await service.insert({
|
|
77
|
+
taskId: 'task-1',
|
|
78
|
+
content: 'Nice work'
|
|
79
|
+
}, user);
|
|
80
|
+
expect(result).toEqual(expect.objectContaining({
|
|
81
|
+
taskId: 'task-1',
|
|
82
|
+
userId: 'author-1',
|
|
83
|
+
content: 'Nice work',
|
|
84
|
+
parentCommentId: null
|
|
85
|
+
}));
|
|
86
|
+
});
|
|
87
|
+
it('creates a reply comment carrying the parentCommentId', async ()=>{
|
|
88
|
+
mockQueryRunner(mockRepo);
|
|
89
|
+
const result = await service.insert({
|
|
90
|
+
taskId: 'task-1',
|
|
91
|
+
content: 'Reply',
|
|
92
|
+
parentCommentId: 'comment-parent-1'
|
|
93
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
94
|
+
expect(result).toEqual(expect.objectContaining({
|
|
95
|
+
parentCommentId: 'comment-parent-1'
|
|
96
|
+
}));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('convertSingleDtoToEntity (via update)', ()=>{
|
|
100
|
+
it('throws NotFoundException when the comment does not exist', async ()=>{
|
|
101
|
+
mockQueryRunner(mockRepo);
|
|
102
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
103
|
+
await expect(service.update({
|
|
104
|
+
id: 'missing',
|
|
105
|
+
content: 'Edited'
|
|
106
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toThrow();
|
|
107
|
+
});
|
|
108
|
+
it('throws ForbiddenException when editing a comment authored by another user', async ()=>{
|
|
109
|
+
mockQueryRunner(mockRepo);
|
|
110
|
+
const existing = buildTaskComment({
|
|
111
|
+
id: 'comment-1',
|
|
112
|
+
userId: 'someone-else'
|
|
113
|
+
});
|
|
114
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
115
|
+
await expect(service.update({
|
|
116
|
+
id: 'comment-1',
|
|
117
|
+
content: 'Edited'
|
|
118
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
119
|
+
id: 'not-the-author'
|
|
120
|
+
}))).rejects.toThrow();
|
|
121
|
+
});
|
|
122
|
+
it('allows the original author to edit their own comment', async ()=>{
|
|
123
|
+
mockQueryRunner(mockRepo);
|
|
124
|
+
const existing = buildTaskComment({
|
|
125
|
+
id: 'comment-1',
|
|
126
|
+
userId: 'author-1',
|
|
127
|
+
content: 'Old'
|
|
128
|
+
});
|
|
129
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
130
|
+
const result = await service.update({
|
|
131
|
+
id: 'comment-1',
|
|
132
|
+
content: 'Updated content'
|
|
133
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
134
|
+
id: 'author-1'
|
|
135
|
+
}));
|
|
136
|
+
expect(result).toEqual(expect.objectContaining({
|
|
137
|
+
id: 'comment-1',
|
|
138
|
+
content: 'Updated content'
|
|
139
|
+
}));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('getFilterQuery', ()=>{
|
|
143
|
+
it('filters by taskId when supplied', async ()=>{
|
|
144
|
+
const qb = (0, _repositorymock.createMockQueryBuilder)();
|
|
145
|
+
const { query, isRaw } = await service.getFilterQuery(qb, {
|
|
146
|
+
taskId: 'task-1'
|
|
147
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
148
|
+
expect(qb.andWhere).toHaveBeenCalledWith('task_comment.taskId = :taskId', {
|
|
149
|
+
taskId: 'task-1'
|
|
150
|
+
});
|
|
151
|
+
expect(isRaw).toBe(false);
|
|
152
|
+
expect(query).toBe(qb);
|
|
153
|
+
});
|
|
154
|
+
it('applies no filter when taskId is absent', async ()=>{
|
|
155
|
+
const qb = (0, _repositorymock.createMockQueryBuilder)();
|
|
156
|
+
await service.getFilterQuery(qb, {}, (0, _loggedusermock.buildMockUser)());
|
|
157
|
+
expect(qb.andWhere).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|