Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions src/controllers/achievement.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const createAchievement = async (req: Request, res: Response) => {

export const updateAchievementById = async (req: Request, res: Response) => {
const achievementId = parseInt(req.params.achievementId);

if (!achievementId || isNaN(achievementId)) {
throw new ApiError("Invalid achievement ID", 400);
}
Expand All @@ -81,7 +82,7 @@ export const updateAchievementById = async (req: Request, res: Response) => {
let imageUrl: string | undefined;

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


const existingAchievement = await achievementService.getAchievementById(achievementId);
if (!existingAchievement) {
throw new ApiError("Achievement not found", 404);
}

if (file) {
imageUrl = await uploadImage(supabase, file, 'achievements', existingAchievement.imageUrl );
imageUrl = await uploadImage(supabase, file, "achievements", existingAchievement.imageUrl);
}


const { memberIds: _, ...updatePayload } = achievementData;

if (imageUrl) {
achievementData.imageUrl = imageUrl;
}

if (
!title &&
!description &&
!achievedAt &&
!imageUrl &&
(!Array.isArray(memberIds) || memberIds.length === 0)
) {
throw new ApiError("At least one field must be provided for update", 400);
}

updatePayload.imageUrl = imageUrl;
}

const hasSomethingToUpdate =
title || description || achievedAt || imageUrl || (Array.isArray(memberIds) && memberIds.length > 0);

if (!hasSomethingToUpdate) {
throw new ApiError("At least one field (title, description, achievedAt, image, or memberIds) must be provided for update", 400);
}

const updatedAchievement = await achievementService.updateAchievementById(
achievementId,
achievementData
updatePayload
);

if (Array.isArray(memberIds) && memberIds.length > 0) {
Expand Down
196 changes: 140 additions & 56 deletions tests/Achievement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,82 +292,166 @@ describe('updateAchievementById', () => {
updatedAt: new Date(),
createdBy: { id: 'admin_123', name: 'Admin' },
updatedBy: null,
members: [
{
member: {
id: 'user_1',
name: 'User One',
email: 'user1@example.com',
profilePhoto: null,
},
},
],
};

const updatedAchievement = {
...baseAchievement,
title: 'Updated Title',
updatedById: 'admin_456',
updatedBy: {
id: 'admin_456',
name: 'Admin Two',
email: 'admin2@example.com',
},
imageUrl: 'https://example.com/uploaded/updated.png',
members: [
{
member: {
id: 'user_2',
name: 'User Two',
email: 'user2@example.com',
profilePhoto: null,
},
},
],
};

const mockReq: any = {
params: { achievementId: '1' },
file: {
originalname: 'updated.png',
buffer: Buffer.from('test-image'),
},
body: {
achievementData: JSON.stringify({
title: 'Updated Title',
updatedById: 'admin_456',
memberIds: ['user_2'],
}),
},
members: [],
};

const mockRes: any = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};

it('should return 200 and updated achievement', async () => {
mockedUploadImage.mockResolvedValue('https://example.com/uploaded/updated.png');
beforeEach(() => {
jest.clearAllMocks();
});

it('should update all fields successfully (200)', async () => {
const updatedAchievement = { ...baseAchievement, title: 'Updated', updatedById: 'admin_456' };
const mockReq: any = {
params: { achievementId: '1' },
file: { originalname: 'img.png', buffer: Buffer.from('123') },
body: {
achievementData: JSON.stringify({
title: 'Updated',
updatedById: 'admin_456',
memberIds: ['user_2'],
}),
},
};

mockedUploadImage.mockResolvedValue('https://updated.com/img.png');
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue(updatedAchievement);
jest.spyOn(achievementService, 'addMembersToAchievement').mockResolvedValue(undefined);

await updateAchievementById(mockReq, mockRes);

expect(mockedUploadImage).toHaveBeenCalled();
expect(achievementService.getAchievementById).toHaveBeenCalledWith(1);
expect(achievementService.updateAchievementById).toHaveBeenCalledWith(1, expect.objectContaining({
title: 'Updated Title',
updatedById: 'admin_456',
imageUrl: 'https://example.com/uploaded/updated.png',
}));
expect(achievementService.addMembersToAchievement).toHaveBeenCalledWith(1, ['user_2']);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({
success: true,
data: updatedAchievement,
});
});

it('should update only title', async () => {
const mockReq: any = {
params: { achievementId: '1' },
body: {
achievementData: JSON.stringify({ title: 'New Title', updatedById: 'admin_456' }),
},
};

jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue({
...baseAchievement,
title: 'New Title',
});

await updateAchievementById(mockReq, mockRes);
expect(achievementService.updateAchievementById).toHaveBeenCalledWith(1, expect.objectContaining({
title: 'New Title',
updatedById: 'admin_456',
}));
});

it('should update only image', async () => {
const mockReq: any = {
params: { achievementId: '1' },
file: { originalname: 'img.png', buffer: Buffer.from('abc') },
body: {
achievementData: JSON.stringify({ updatedById: 'admin_456' }),
},
};

mockedUploadImage.mockResolvedValue('https://updated.com/img.png');
jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue({
...baseAchievement,
imageUrl: 'https://updated.com/img.png',
updatedById: 'admin_456',
});

await updateAchievementById(mockReq, mockRes);
expect(mockedUploadImage).toHaveBeenCalled();
});

it('should update only memberIds', async () => {
const mockReq: any = {
params: { achievementId: '1' },
body: {
achievementData: JSON.stringify({ updatedById: 'admin_456', memberIds: ['user_3'] }),
},
};

jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);
jest.spyOn(achievementService, 'updateAchievementById').mockResolvedValue({
...baseAchievement,
updatedById: 'admin_456',
});
jest.spyOn(achievementService, 'addMembersToAchievement').mockResolvedValue(undefined);

await updateAchievementById(mockReq, mockRes);
expect(achievementService.addMembersToAchievement).toHaveBeenCalledWith(1, ['user_3']);
});

it('should return 400 if updatedById is missing', async () => {
const mockReq: any = {
params: { achievementId: '1' },
body: {
achievementData: JSON.stringify({ title: 'No Updater' }),
},
};

await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
});

it('should return 400 if achievementId is invalid', async () => {
const mockReq: any = {
params: { achievementId: 'abc' },
body: {
achievementData: JSON.stringify({ title: 'Updated', updatedById: 'admin_456' }),
},
};

await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
});

it('should return 404 if achievement not found', async () => {
const mockReq: any = {
params: { achievementId: '999' },
body: {
achievementData: JSON.stringify({ title: 'Missing', updatedById: 'admin_456' }),
},
};

jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(null);

await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
});

it('should return 400 if no fields are provided for update', async () => {
const mockReq: any = {
params: { achievementId: '1' },
body: {
achievementData: JSON.stringify({ updatedById: 'admin_456' }),
},
};

jest.spyOn(achievementService, 'getAchievementById').mockResolvedValue(baseAchievement);

await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
});

it('should return 400 if achievementData is invalid JSON', async () => {
const mockReq: any = {
params: { achievementId: '1' },
body: {
achievementData: '{ invalid JSON }',
},
};

await expect(updateAchievementById(mockReq, mockRes)).rejects.toThrow(ApiError);
});
});

const mockedDeleteImage = deleteImage as jest.Mock;
Expand Down