Skip to content

Commit 4bd12e7

Browse files
Merge pull request #26 from call-0f-code/achievement-fixes
Achievement fixes
2 parents 81ddb67 + 86fd720 commit 4bd12e7

2 files changed

Lines changed: 158 additions & 75 deletions

File tree

src/controllers/achievement.controller.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const createAchievement = async (req: Request, res: Response) => {
7373

7474
export const updateAchievementById = async (req: Request, res: Response) => {
7575
const achievementId = parseInt(req.params.achievementId);
76+
7677
if (!achievementId || isNaN(achievementId)) {
7778
throw new ApiError("Invalid achievement ID", 400);
7879
}
@@ -81,7 +82,7 @@ export const updateAchievementById = async (req: Request, res: Response) => {
8182
let imageUrl: string | undefined;
8283

8384
let achievementData = req.body.achievementData;
84-
if (typeof achievementData === 'string') {
85+
if (typeof achievementData === "string") {
8586
try {
8687
achievementData = JSON.parse(achievementData);
8788
} catch (e) {
@@ -95,33 +96,31 @@ export const updateAchievementById = async (req: Request, res: Response) => {
9596
throw new ApiError("updatedById is required", 400);
9697
}
9798

98-
9999
const existingAchievement = await achievementService.getAchievementById(achievementId);
100100
if (!existingAchievement) {
101101
throw new ApiError("Achievement not found", 404);
102102
}
103-
103+
104104
if (file) {
105-
imageUrl = await uploadImage(supabase, file, 'achievements', existingAchievement.imageUrl );
105+
imageUrl = await uploadImage(supabase, file, "achievements", existingAchievement.imageUrl);
106106
}
107-
107+
108+
const { memberIds: _, ...updatePayload } = achievementData;
109+
108110
if (imageUrl) {
109-
achievementData.imageUrl = imageUrl;
110-
}
111-
112-
if (
113-
!title &&
114-
!description &&
115-
!achievedAt &&
116-
!imageUrl &&
117-
(!Array.isArray(memberIds) || memberIds.length === 0)
118-
) {
119-
throw new ApiError("At least one field must be provided for update", 400);
120-
}
121-
111+
updatePayload.imageUrl = imageUrl;
112+
}
113+
114+
const hasSomethingToUpdate =
115+
title || description || achievedAt || imageUrl || (Array.isArray(memberIds) && memberIds.length > 0);
116+
117+
if (!hasSomethingToUpdate) {
118+
throw new ApiError("At least one field (title, description, achievedAt, image, or memberIds) must be provided for update", 400);
119+
}
120+
122121
const updatedAchievement = await achievementService.updateAchievementById(
123122
achievementId,
124-
achievementData
123+
updatePayload
125124
);
126125

127126
if (Array.isArray(memberIds) && memberIds.length > 0) {

tests/Achievement.test.ts

Lines changed: 140 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -292,82 +292,166 @@ describe('updateAchievementById', () => {
292292
updatedAt: new Date(),
293293
createdBy: { id: 'admin_123', name: 'Admin' },
294294
updatedBy: null,
295-
members: [
296-
{
297-
member: {
298-
id: 'user_1',
299-
name: 'User One',
300-
email: 'user1@example.com',
301-
profilePhoto: null,
302-
},
303-
},
304-
],
305-
};
306-
307-
const updatedAchievement = {
308-
...baseAchievement,
309-
title: 'Updated Title',
310-
updatedById: 'admin_456',
311-
updatedBy: {
312-
id: 'admin_456',
313-
name: 'Admin Two',
314-
email: 'admin2@example.com',
315-
},
316-
imageUrl: 'https://example.com/uploaded/updated.png',
317-
members: [
318-
{
319-
member: {
320-
id: 'user_2',
321-
name: 'User Two',
322-
email: 'user2@example.com',
323-
profilePhoto: null,
324-
},
325-
},
326-
],
327-
};
328-
329-
const mockReq: any = {
330-
params: { achievementId: '1' },
331-
file: {
332-
originalname: 'updated.png',
333-
buffer: Buffer.from('test-image'),
334-
},
335-
body: {
336-
achievementData: JSON.stringify({
337-
title: 'Updated Title',
338-
updatedById: 'admin_456',
339-
memberIds: ['user_2'],
340-
}),
341-
},
295+
members: [],
342296
};
343297

344298
const mockRes: any = {
345299
status: jest.fn().mockReturnThis(),
346300
json: jest.fn(),
347301
};
348302

349-
it('should return 200 and updated achievement', async () => {
350-
mockedUploadImage.mockResolvedValue('https://example.com/uploaded/updated.png');
303+
beforeEach(() => {
304+
jest.clearAllMocks();
305+
});
306+
307+
it('should update all fields successfully (200)', async () => {
308+
const updatedAchievement = { ...baseAchievement, title: 'Updated', updatedById: 'admin_456' };
309+
const mockReq: any = {
310+
params: { achievementId: '1' },
311+
file: { originalname: 'img.png', buffer: Buffer.from('123') },
312+
body: {
313+
achievementData: JSON.stringify({
314+
title: 'Updated',
315+
updatedById: 'admin_456',
316+
memberIds: ['user_2'],
317+
}),
318+
},
319+
};
320+
321+
mockedUploadImage.mockResolvedValue('https://updated.com/img.png');
351322
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
352323
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue(updatedAchievement);
353324
jest.spyOn(achievementService, 'addMembersToAchievement').mockResolvedValue(undefined);
354325

355326
await updateAchievementById(mockReq, mockRes);
356327

357328
expect(mockedUploadImage).toHaveBeenCalled();
358-
expect(achievementService.getAchievementById).toHaveBeenCalledWith(1);
359-
expect(achievementService.updateAchievementById).toHaveBeenCalledWith(1, expect.objectContaining({
360-
title: 'Updated Title',
361-
updatedById: 'admin_456',
362-
imageUrl: 'https://example.com/uploaded/updated.png',
363-
}));
364-
expect(achievementService.addMembersToAchievement).toHaveBeenCalledWith(1, ['user_2']);
365329
expect(mockRes.status).toHaveBeenCalledWith(200);
366330
expect(mockRes.json).toHaveBeenCalledWith({
367331
success: true,
368332
data: updatedAchievement,
369333
});
370334
});
335+
336+
it('should update only title', async () => {
337+
const mockReq: any = {
338+
params: { achievementId: '1' },
339+
body: {
340+
achievementData: JSON.stringify({ title: 'New Title', updatedById: 'admin_456' }),
341+
},
342+
};
343+
344+
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
345+
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue({
346+
...baseAchievement,
347+
title: 'New Title',
348+
});
349+
350+
await updateAchievementById(mockReq, mockRes);
351+
expect(achievementService.updateAchievementById).toHaveBeenCalledWith(1, expect.objectContaining({
352+
title: 'New Title',
353+
updatedById: 'admin_456',
354+
}));
355+
});
356+
357+
it('should update only image', async () => {
358+
const mockReq: any = {
359+
params: { achievementId: '1' },
360+
file: { originalname: 'img.png', buffer: Buffer.from('abc') },
361+
body: {
362+
achievementData: JSON.stringify({ updatedById: 'admin_456' }),
363+
},
364+
};
365+
366+
mockedUploadImage.mockResolvedValue('https://updated.com/img.png');
367+
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
368+
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue({
369+
...baseAchievement,
370+
imageUrl: 'https://updated.com/img.png',
371+
updatedById: 'admin_456',
372+
});
373+
374+
await updateAchievementById(mockReq, mockRes);
375+
expect(mockedUploadImage).toHaveBeenCalled();
376+
});
377+
378+
it('should update only memberIds', async () => {
379+
const mockReq: any = {
380+
params: { achievementId: '1' },
381+
body: {
382+
achievementData: JSON.stringify({ updatedById: 'admin_456', memberIds: ['user_3'] }),
383+
},
384+
};
385+
386+
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
387+
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue({
388+
...baseAchievement,
389+
updatedById: 'admin_456',
390+
});
391+
jest.spyOn(achievementService, 'addMembersToAchievement').mockResolvedValue(undefined);
392+
393+
await updateAchievementById(mockReq, mockRes);
394+
expect(achievementService.addMembersToAchievement).toHaveBeenCalledWith(1, ['user_3']);
395+
});
396+
397+
it('should return 400 if updatedById is missing', async () => {
398+
const mockReq: any = {
399+
params: { achievementId: '1' },
400+
body: {
401+
achievementData: JSON.stringify({ title: 'No Updater' }),
402+
},
403+
};
404+
405+
await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
406+
});
407+
408+
it('should return 400 if achievementId is invalid', async () => {
409+
const mockReq: any = {
410+
params: { achievementId: 'abc' },
411+
body: {
412+
achievementData: JSON.stringify({ title: 'Updated', updatedById: 'admin_456' }),
413+
},
414+
};
415+
416+
await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
417+
});
418+
419+
it('should return 404 if achievement not found', async () => {
420+
const mockReq: any = {
421+
params: { achievementId: '999' },
422+
body: {
423+
achievementData: JSON.stringify({ title: 'Missing', updatedById: 'admin_456' }),
424+
},
425+
};
426+
427+
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(null);
428+
429+
await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
430+
});
431+
432+
it('should return 400 if no fields are provided for update', async () => {
433+
const mockReq: any = {
434+
params: { achievementId: '1' },
435+
body: {
436+
achievementData: JSON.stringify({ updatedById: 'admin_456' }),
437+
},
438+
};
439+
440+
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
441+
442+
await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
443+
});
444+
445+
it('should return 400 if achievementData is invalid JSON', async () => {
446+
const mockReq: any = {
447+
params: { achievementId: '1' },
448+
body: {
449+
achievementData: '{ invalid JSON }',
450+
},
451+
};
452+
453+
await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
454+
});
371455
});
372456

373457
const mockedDeleteImage = deleteImage as jest.Mock;

0 commit comments

Comments
 (0)