diff --git a/admin-frontend/src/components/announcements/AnnouncementActions.vue b/admin-frontend/src/components/announcements/AnnouncementActions.vue index f77ace3f8..7070421ac 100644 --- a/admin-frontend/src/components/announcements/AnnouncementActions.vue +++ b/admin-frontend/src/components/announcements/AnnouncementActions.vue @@ -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(); @@ -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.', + ); + } }; diff --git a/admin-frontend/src/components/announcements/__tests__/AnnouncementActions.spec.ts b/admin-frontend/src/components/announcements/__tests__/AnnouncementActions.spec.ts index 4db883a74..4c6115b7d 100644 --- a/admin-frontend/src/components/announcements/__tests__/AnnouncementActions.spec.ts +++ b/admin-frontend/src/components/announcements/__tests__/AnnouncementActions.spec.ts @@ -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'; @@ -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(); + }); + }); + }); }); diff --git a/admin-frontend/src/services/__tests__/apiService.spec.ts b/admin-frontend/src/services/__tests__/apiService.spec.ts index 7a4e23a60..c23173320 100644 --- a/admin-frontend/src/services/__tests__/apiService.spec.ts +++ b/admin-frontend/src/services/__tests__/apiService.spec.ts @@ -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 () => { diff --git a/admin-frontend/src/services/apiService.ts b/admin-frontend/src/services/apiService.ts index aa805d490..8957caa1a 100644 --- a/admin-frontend/src/services/apiService.ts +++ b/admin-frontend/src/services/apiService.ts @@ -7,6 +7,7 @@ import { UserInvite, } from '../types'; import { + Announcement, AnnouncementFilterType, AnnouncementFormValue, AnnouncementSortType, @@ -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. diff --git a/backend/src/v1/routes/announcement-routes.spec.ts b/backend/src/v1/routes/announcement-routes.spec.ts index 600e53e1a..74caf6741 100644 --- a/backend/src/v1/routes/announcement-routes.spec.ts +++ b/backend/src/v1/routes/announcement-routes.spec.ts @@ -14,6 +14,7 @@ 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); @@ -21,6 +22,7 @@ jest.mock('../services/announcements-service', () => ({ patchAnnouncements: (...args) => mockPatchAnnouncements(...args), createAnnouncement: (...args) => mockCreateAnnouncement(...args), updateAnnouncement: (...args) => mockUpdateAnnouncement(...args), + getAnnouncementById: (...args) => mockGetAnnouncementById(...args), })); jest.mock('../middlewares/authorization/authenticate-admin', () => ({ @@ -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(); + }); + }); + }) }); diff --git a/backend/src/v1/routes/announcement-routes.ts b/backend/src/v1/routes/announcement-routes.ts index 38267a161..7decfe946 100644 --- a/backend/src/v1/routes/announcement-routes.ts +++ b/backend/src/v1/routes/announcement-routes.ts @@ -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'; @@ -9,6 +9,7 @@ import { useUpload } from '../middlewares/storage/upload'; import { useValidate } from '../middlewares/validations'; import { createAnnouncement, + getAnnouncementById, getAnnouncements, patchAnnouncements, updateAnnouncement, @@ -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; diff --git a/backend/src/v1/services/announcements-service.spec.ts b/backend/src/v1/services/announcements-service.spec.ts index b9b70a66f..b1f0a22e4 100644 --- a/backend/src/v1/services/announcements-service.spec.ts +++ b/backend/src/v1/services/announcements-service.spec.ts @@ -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), @@ -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: [], @@ -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, }), }), ); @@ -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 }, + }); + }); + }); }); diff --git a/backend/src/v1/services/announcements-service.ts b/backend/src/v1/services/announcements-service.ts index 0595b5a7a..29f55c265 100644 --- a/backend/src/v1/services/announcements-service.ts +++ b/backend/src/v1/services/announcements-service.ts @@ -147,6 +147,22 @@ export const getAnnouncements = async ( }; }; +/** + * Get announcement by id + * @param id + * @returns + */ +export const getAnnouncementById = async (id: string): Promise => { + 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. @@ -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 }, },