Skip to content

Commit

Permalink
feat: Add error handling for failed announcement retrieval
Browse files Browse the repository at this point in the history
The code changes in this commit add error handling for the case when the retrieval of an announcement fails. When the `editAnnouncement` function is called, it now makes an API request to retrieve the announcement using the `getAnnouncement` method from the `ApiService`. If the retrieval is successful, the announcement is set in the `announcementSelectionStore` and the user is redirected to the edit announcement page. However, if the retrieval fails, an error message is displayed using the `NotificationService`.

This commit also includes recent user commits and recent repository commits for reference.
  • Loading branch information
goemen committed Sep 5, 2024
1 parent 8ed623e commit 7de79f8
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ import { useAnnouncementSelectionStore } from '../../store/modules/announcementS
import { ref } from 'vue';
import { NotificationService } from '../../services/notificationService';
import { useRouter } from 'vue-router';
import { Announcement } from '../../types/announcements';
const router = useRouter();
const announcementSelectionStore = useAnnouncementSelectionStore();
const { announcement } = defineProps<{
announcement: any;
const props = defineProps<{
announcement: Announcement;
}>();
const announcementSearchStore = useAnnouncementSearchStore();
Expand Down Expand Up @@ -116,8 +117,18 @@ async function unpublishAnnouncement(announcementId: string) {
}
}
const editAnnouncement = () => {
announcementSelectionStore.setAnnouncement(announcement);
router.push('/edit-announcement');
const editAnnouncement = async () => {
const { announcement_id } = props.announcement;
try {
const announcement = await ApiService.getAnnouncement(announcement_id);
announcementSelectionStore.setAnnouncement(announcement);
router.push(`/edit-announcement`);
} catch (e) {
console.error(e);
NotificationService.pushNotificationError(
'Error',
'An error occurred while trying to load the announcement.',
);
}
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import ApiService from '../../../services/apiService';
import { NotificationService } from '../../../services/notificationService';
import { Announcement } from '../../../types/announcements';
import AnnouncementActions from '../AnnouncementActions.vue';

Expand Down Expand Up @@ -143,4 +144,36 @@ describe('AnnouncementActions', () => {
});
});
});

describe('edit announcement', () => {
it('sets the announcement to edit mode', async () => {
const apiSpy = vi
.spyOn(ApiService, 'getAnnouncement')
.mockImplementation(() => {
return Promise.resolve(mockDraftAnnouncement as any);
});
await wrapper.vm.editAnnouncement();

expect(apiSpy).toHaveBeenCalledWith(
mockDraftAnnouncement.announcement_id,
);
});

describe('when the announcement is not successfully retrieved', () => {
it('shows an error message', async () => {
vi.spyOn(ApiService, 'getAnnouncement').mockImplementation(() => {
return Promise.reject();
});

const errorSnackbarSpy = vi.spyOn(
NotificationService,
'pushNotificationError',
);

await wrapper.vm.editAnnouncement();

expect(errorSnackbarSpy).toHaveBeenCalled();
});
});
});
});
28 changes: 28 additions & 0 deletions admin-frontend/src/services/__tests__/apiService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,34 @@ describe('ApiService', () => {
});
});

describe('getAnnouncement', () => {
it('returns an announcement', async () => {
const mockBackendResponse = { title: 'test' };
const mockAxiosResponse = {
data: mockBackendResponse,
};
vi.spyOn(ApiService.apiAxios, 'get').mockResolvedValueOnce(
mockAxiosResponse,
);

const resp = await ApiService.getAnnouncement('1');
expect(resp).toEqual(mockBackendResponse);
});

describe('when the data are not successfully retrieved from the backend', () => {
it('returns a rejected promise', async () => {
const mockAxiosError = new AxiosError();
vi.spyOn(ApiService.apiAxios, 'get').mockRejectedValueOnce(
mockAxiosError,
);

await expect(ApiService.getAnnouncement('1')).rejects.toEqual(
mockAxiosError,
);
});
});
});

describe('clamavScanFile', () => {
describe('when the given file is valid', () => {
it('returns a response', async () => {
Expand Down
20 changes: 20 additions & 0 deletions admin-frontend/src/services/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
UserInvite,
} from '../types';
import {
Announcement,
AnnouncementFilterType,
AnnouncementFormValue,
AnnouncementSortType,
Expand Down Expand Up @@ -337,6 +338,25 @@ export default {
}
},

async getAnnouncement(id: string) {
try {
const { data } = await apiAxios.get<
Announcement & {
announcement_resource: {
resource_type: string;
display_name: string;
resource_url: string;
attachment_file_id: string;
}[];
}
>(`${ApiRoutes.ANNOUNCEMENTS}/${id}`);
return data;
} catch (e) {
console.log(`Failed to get announcement from API - ${e}`);
throw e;
}
},

/**
* Download a list of reports in csv format. This method also causes
* the browser to save the resulting file.
Expand Down
20 changes: 20 additions & 0 deletions backend/src/v1/routes/announcement-routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ const mockGetAnnouncements = jest.fn().mockResolvedValue({
const mockPatchAnnouncements = jest.fn();
const mockCreateAnnouncement = jest.fn();
const mockUpdateAnnouncement = jest.fn();
const mockGetAnnouncementById = jest.fn();
jest.mock('../services/announcements-service', () => ({
getAnnouncements: (...args) => {
return mockGetAnnouncements(...args);
},
patchAnnouncements: (...args) => mockPatchAnnouncements(...args),
createAnnouncement: (...args) => mockCreateAnnouncement(...args),
updateAnnouncement: (...args) => mockUpdateAnnouncement(...args),
getAnnouncementById: (...args) => mockGetAnnouncementById(...args),
}));

jest.mock('../middlewares/authorization/authenticate-admin', () => ({
Expand Down Expand Up @@ -431,4 +433,22 @@ describe('announcement-routes', () => {
});
});
});

describe('GET /:id - get announcement by id', () => {
it('should return 200', async () => {
const response = await request(app).get('/123');
expect(response.status).toBe(200);
expect(mockGetAnnouncementById).toHaveBeenCalledWith("123");
});

describe('when service throws error', () => {
it('should return 400', async () => {
mockGetAnnouncementById.mockRejectedValue(new Error('Invalid request'));
const response = await request(app).get('/123');

expect(response.status).toBe(400);
expect(response.body.error).toBeDefined();
});
});
})
});
13 changes: 12 additions & 1 deletion backend/src/v1/routes/announcement-routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Router } from 'express';
import { Request, Router } from 'express';
import formData from 'express-form-data';
import os from 'os';
import { APP_ANNOUNCEMENTS_FOLDER } from '../../constants/admin';
Expand All @@ -9,6 +9,7 @@ import { useUpload } from '../middlewares/storage/upload';
import { useValidate } from '../middlewares/validations';
import {
createAnnouncement,
getAnnouncementById,
getAnnouncements,
patchAnnouncements,
updateAnnouncement,
Expand Down Expand Up @@ -137,4 +138,14 @@ router.put(
},
);

router.get('/:id', authenticateAdmin(), async (req: Request, res) => {
try {
const announcement = await getAnnouncementById(req.params.id);
return res.json(announcement);
} catch (error) {
logger.error(error);
res.status(400).json({ message: 'Invalid request', error });
}
});

export default router;
17 changes: 14 additions & 3 deletions backend/src/v1/services/announcements-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jest.mock('../prisma/prisma-client', () => ({
count: jest.fn().mockResolvedValue(2),
updateMany: (...args) => mockUpdateMany(...args),
create: (...args) => mockCreateAnnouncement(...args),
findUniqueOrThrow: (...args) => mockFindUniqueOrThrow(...args),
},
announcement_history: {
create: (...args) => mockHistoryCreate(...args),
Expand Down Expand Up @@ -832,7 +833,7 @@ describe('AnnouncementsService', () => {
});
});

it('should default to undefined dates', async () => {
it('should default to null dates', async () => {
mockFindUniqueOrThrow.mockResolvedValue({
id: 'announcement-id',
announcement_resource: [],
Expand All @@ -855,8 +856,8 @@ describe('AnnouncementsService', () => {
expect.objectContaining({
where: { announcement_id: 'announcement-id' },
data: expect.objectContaining({
expires_on: undefined,
published_on: undefined,
expires_on: null,
published_on: null,
}),
}),
);
Expand Down Expand Up @@ -912,4 +913,14 @@ describe('AnnouncementsService', () => {
});
});
});

describe('getAnnouncementById', () => {
it('should return announcement by id', async () => {
await AnnouncementService.getAnnouncementById('1');
expect(mockFindUniqueOrThrow).toHaveBeenCalledWith({
where: { announcement_id: '1' },
include: { announcement_resource: true },
});
});
});
});
20 changes: 18 additions & 2 deletions backend/src/v1/services/announcements-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ export const getAnnouncements = async (
};
};

/**
* Get announcement by id
* @param id
* @returns
*/
export const getAnnouncementById = async (id: string): Promise<announcement> => {
return prisma.announcement.findUniqueOrThrow({
where: {
announcement_id: id,
},
include: {
announcement_resource: true,
},
});
}

/**
* Patch announcements by ids.
* This method also copies the original record into the announcement history table.
Expand Down Expand Up @@ -411,8 +427,8 @@ export const updateAnnouncement = async (
},
published_on: !isEmpty(input.published_on)
? input.published_on
: undefined,
expires_on: !isEmpty(input.expires_on) ? input.expires_on : undefined,
: null,
expires_on: !isEmpty(input.expires_on) ? input.expires_on : null,
admin_user_announcement_updated_byToadmin_user: {
connect: { admin_user_id: currentUserId },
},
Expand Down

0 comments on commit 7de79f8

Please sign in to comment.