Skip to content

Commit

Permalink
Merge branch 'main' into PIMS-1956-SingleSelectFilters
Browse files Browse the repository at this point in the history
  • Loading branch information
GrahamS-Quartech authored Aug 14, 2024
2 parents 7c0cff0 + bf20cda commit 078448e
Show file tree
Hide file tree
Showing 19 changed files with 349 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import notificationServices from '@/services/notifications/notificationServices';
import notificationServices, {
NotificationStatus,
} from '@/services/notifications/notificationServices';
import userServices from '@/services/users/usersServices';
import { SSOUser } from '@bcgov/citz-imb-sso-express';
import { Request, Response } from 'express';
Expand Down Expand Up @@ -48,3 +50,39 @@ export const getNotificationsByProjectId = async (req: Request, res: Response) =
return res.status(500).send({ message: 'Error fetching notifications' });
}
};

export const resendNotificationById = async (req: Request, res: Response) => {
const id = Number(req.params.id);
const notification = await notificationServices.getNotificationById(id);
if (!notification) {
return res.status(404).send('Notification not found.');
}
const kcUser = req.user;
if (!isAdmin(kcUser)) {
return res.status(403).send({ message: 'User is not authorized to access this endpoint.' });
}
const resultantNotification = await notificationServices.sendNotification(notification, kcUser);
const user = await userServices.getUser(kcUser.preferred_username);
const updatedNotification = await notificationServices.updateNotificationStatus(
resultantNotification.Id,
user,
);
return res.status(200).send(updatedNotification);
};

export const cancelNotificationById = async (req: Request, res: Response) => {
const id = Number(req.params.id);
const notification = await notificationServices.getNotificationById(id);
if (!notification) {
return res.status(404).send('Notification not found.');
}
const kcUser = req.user;
if (!isAdmin(kcUser)) {
return res.status(403).send({ message: 'User is not authorized to access this endpoint.' });
}
const resultantNotification = await notificationServices.cancelNotificationById(notification.Id);
if (resultantNotification.Status !== NotificationStatus.Cancelled) {
return res.status(400).send(resultantNotification);
}
return res.status(200).send(resultantNotification);
};
12 changes: 10 additions & 2 deletions express-api/src/routes/notificationsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@ export const DISPOSAL_API_ROUTE = '/projects/disposal';
export const NOTIFICATION_QUEUE_ROUTE = '/queue';
export const NOTIFICATION_TEMPLATE_ROUTE = '/templates';

const { getNotificationsByProjectId } = controllers;
const { getNotificationsByProjectId, resendNotificationById, cancelNotificationById } = controllers;

//I believe that the IDs used in these routes are actually the project ID, even though the structure here sort of implies
//that it might be an individual "notification id".
router
.route(`${NOTIFICATION_QUEUE_ROUTE}`)
.route(NOTIFICATION_QUEUE_ROUTE)
.get(activeUserCheck, catchErrors(getNotificationsByProjectId));

router
.route(`${NOTIFICATION_QUEUE_ROUTE}/:id`)
.put(activeUserCheck, catchErrors(resendNotificationById));

router
.route(`${NOTIFICATION_QUEUE_ROUTE}/:id`)
.delete(activeUserCheck, catchErrors(cancelNotificationById));

export default router;
2 changes: 1 addition & 1 deletion express-api/src/services/ches/chesServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ const getStatusByIdAsync = async (messageId: string): Promise<IChesStatusRespons
if (!response) {
throw new Error(`Failed to fetch status for messageId ${messageId}`);
}
return await response;
return response;
} catch (error) {
logger.error(`Error fetching status for messageId ${messageId}:`, error);
return null;
Expand Down
21 changes: 21 additions & 0 deletions express-api/src/services/notifications/notificationServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,25 @@ const generateProjectWatchNotifications = async (
return notificationsInserted;
};

const cancelNotificationById = async (id: number) => {
const notification = await AppDataSource.getRepository(NotificationQueue).findOne({
where: { Id: id },
});
const chesResult = await chesServices.cancelEmailByIdAsync(notification.ChesMessageId);
if (chesResult.status === 'cancelled') {
return AppDataSource.getRepository(NotificationQueue).save({
Id: notification.Id,
Status: NotificationStatus.Cancelled,
});
} else {
return notification;
}
};

const getNotificationById = async (id: number) => {
return AppDataSource.getRepository(NotificationQueue).findOne({ where: { Id: id } });
};

const notificationServices = {
generateProjectNotifications,
generateAccessRequestNotification,
Expand All @@ -698,6 +717,8 @@ const notificationServices = {
updateNotificationStatus,
getProjectNotificationsInQueue,
convertChesStatusToNotificationStatus,
getNotificationById,
cancelNotificationById,
cancelProjectNotifications,
};

Expand Down
13 changes: 5 additions & 8 deletions express-api/tests/testUtils/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ import { Task } from '@/typeorm/Entities/Task';
import { ProjectTask } from '@/typeorm/Entities/ProjectTask';
import { ProjectStatusNotification } from '@/typeorm/Entities/ProjectStatusNotification';
import { NotificationQueue } from '@/typeorm/Entities/NotificationQueue';
import {
NotificationAudience,
NotificationStatus,
} from '@/services/notifications/notificationServices';
import { NotificationAudience } from '@/services/notifications/notificationServices';
import { NotificationTemplate } from '@/typeorm/Entities/NotificationTemplate';
import { BuildingFiscal } from '@/typeorm/Entities/BuildingFiscal';
import { BuildingEvaluation } from '@/typeorm/Entities/BuildingEvaluation';
Expand Down Expand Up @@ -916,13 +913,13 @@ export const produceNotificationQueue = (props?: Partial<NotificationQueue>) =>
const queue: NotificationQueue = {
Id: faker.number.int(),
Key: randomUUID(),
Status: NotificationStatus.Pending,
Priority: EmailPriority.Normal,
Encoding: EmailEncoding.Utf8,
Status: 1,
Priority: 'normal',
Encoding: 'utf-8',
SendOn: new Date(),
To: faker.internet.email(),
Subject: faker.lorem.word(),
BodyType: EmailBody.Html,
BodyType: 'html',
Body: faker.lorem.sentences(),
Bcc: '',
Cc: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import {
produceUser,
produceSSO,
produceProject,
produceNotificationQueue,
} from 'tests/testUtils/factories';
import projectServices from '@/services/projects/projectsServices';
import { randomUUID } from 'crypto';
import { NotificationQueue } from '@/typeorm/Entities/NotificationQueue';
import { Roles } from '@/constants/roles';
import { NotificationStatus } from '@/services/notifications/notificationServices';

const _getUser = jest
.fn()
Expand All @@ -19,14 +24,33 @@ const _getProjectById = jest.fn().mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async (_id: number) => ({ AgencyId: 1 }),
);
const _getNotifById = jest
.fn()
.mockImplementation((id: number) => produceNotificationQueue({ Id: id }));
const _cancelNotifById = jest
.fn()
.mockImplementation((id: number) => produceNotificationQueue({ Id: id, Status: 2 }));
const _updateNotifStatus = jest
.fn()
.mockImplementation((id: number) => produceNotificationQueue({ Id: id }));

jest.mock('@/services/users/usersServices', () => ({
getAgencies: () => _getAgencies(),
getUser: () => _getUser(),
}));

jest.mock('@/services/notifications/notificationServices', () => ({
...jest.requireActual('@/services/notifications/notificationServices'),
getProjectNotificationsInQueue: () => _getProjectNotificationsInQueue(),
getNotificationById: (id: number) => _getNotifById(id),
cancelNotificationById: (id: number) => _cancelNotifById(id),
sendNotification: (notification: NotificationQueue) =>
produceNotificationQueue({
...notification,
ChesMessageId: randomUUID(),
ChesTransactionId: randomUUID(),
}),
updateNotificationStatus: (id: number) => _updateNotifStatus(id),
}));

jest.mock('@/services/projects/projectsServices', () => ({
Expand All @@ -42,58 +66,116 @@ describe('UNIT - Testing controllers for notifications routes.', () => {
mockResponse = mockRes;
});

it('should return 400 if filter parsing fails', async () => {
mockRequest.query = { invalidField: 'invalidValue' };
describe('getNotificationsByProjectId', () => {
it('should return 400 if filter parsing fails', async () => {
mockRequest.query = { invalidField: 'invalidValue' };

await controllers.getNotificationsByProjectId(mockRequest, mockResponse);
await controllers.getNotificationsByProjectId(mockRequest, mockResponse);

expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.send).toHaveBeenCalledWith({ message: 'Could not parse filter.' });
});
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.send).toHaveBeenCalledWith({ message: 'Could not parse filter.' });
});

it('should return 400 if no valid filter provided', async () => {
await controllers.getNotificationsByProjectId(mockRequest, mockResponse);
it('should return 400 if no valid filter provided', async () => {
await controllers.getNotificationsByProjectId(mockRequest, mockResponse);

expect(mockResponse.statusValue).toBe(400);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.send).toHaveBeenCalledWith({
message: 'Could not parse filter.',
expect(mockResponse.statusValue).toBe(400);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.send).toHaveBeenCalledWith({
message: 'Could not parse filter.',
});
});
});

it('should return 403 if user is not authorized', async () => {
const kcUser = produceSSO();
mockRequest.user = kcUser;
mockRequest.query = { projectId: '1' };
it('should return 403 if user is not authorized', async () => {
const kcUser = produceSSO();
mockRequest.user = kcUser;
mockRequest.query = { projectId: '1' };

await controllers.getNotificationsByProjectId(mockRequest, mockResponse);
await controllers.getNotificationsByProjectId(mockRequest, mockResponse);

expect(mockResponse.status).toHaveBeenCalledWith(403);
});
expect(mockResponse.status).toHaveBeenCalledWith(403);
});

it('should return 200 and notifications if user is authorized', async () => {
const mockRequest = {
query: { projectId: '123' },
user: { agencies: [1] },
} as unknown as Request;
it('should return 200 and notifications if user is authorized', async () => {
const mockRequest = {
query: { projectId: '123' },
user: { agencies: [1] },
} as unknown as Request;

const mockProject = produceProject({
Id: 123,
AgencyId: 1,
});
const mockProject = produceProject({
Id: 123,
AgencyId: 1,
});

const getProjectByIdSpy = jest
.spyOn(projectServices, 'getProjectById')
.mockResolvedValueOnce(mockProject);
const getProjectByIdSpy = jest
.spyOn(projectServices, 'getProjectById')
.mockResolvedValueOnce(mockProject);

const mockResponse = {
status: jest.fn().mockReturnThis(),
send: jest.fn(),
} as unknown as Response;
const mockResponse = {
status: jest.fn().mockReturnThis(),
send: jest.fn(),
} as unknown as Response;

await controllers.getNotificationsByProjectId(mockRequest, mockResponse);
await controllers.getNotificationsByProjectId(mockRequest, mockResponse);

expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(getProjectByIdSpy).toHaveBeenCalledWith(123);
expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(getProjectByIdSpy).toHaveBeenCalledWith(123);
});
});
describe('resendNotifcationById', () => {
it('should read a notification and try to send it through ches again, status 200', async () => {
mockRequest.params.id = '1';
mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] });
await controllers.resendNotificationById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Id).toBe(1);
});
it('should 404 if notif not found', async () => {
mockRequest.params.id = '1';
mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] });
_getNotifById.mockImplementationOnce(() => null);
await controllers.resendNotificationById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(404);
});
it('should 403 if user lacks permissions', async () => {
mockRequest.params.id = '1';
mockRequest.user = produceSSO({ client_roles: [] });
await controllers.resendNotificationById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(403);
});
});
describe('cancelNotificationById', () => {
it('should try to cancel a notification, status 200', async () => {
mockRequest.params.id = '1';
mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] });
await controllers.cancelNotificationById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Id).toBe(1);
expect(mockResponse.sendValue.Status).toBe(NotificationStatus.Cancelled);
});
it('should 404 if no notif found', async () => {
mockRequest.params.id = '1';
mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] });
_getNotifById.mockImplementationOnce(() => null);
await controllers.cancelNotificationById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(404);
});
it('should 400 if the notification came back with non-cancelled status', async () => {
mockRequest.params.id = '1';
mockRequest.user = produceSSO({ client_roles: [Roles.ADMIN] });
_cancelNotifById.mockImplementationOnce((id: number) =>
produceNotificationQueue({ Id: id, Status: NotificationStatus.Completed }),
);
await controllers.cancelNotificationById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(400);
expect(mockResponse.sendValue.Id).toBe(1);
expect(mockResponse.sendValue.Status).not.toBe(NotificationStatus.Cancelled);
});
it('should 403 if user lacks permissions', async () => {
mockRequest.params.id = '1';
mockRequest.user = produceSSO({ client_roles: [] });
await controllers.cancelNotificationById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(403);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,41 @@ describe('generateProjectWatchNotifications', () => {
expect(result.length).toBe(1);
});
});
describe('cancelNotificationById', () => {
it('should cancel a notificaion', async () => {
_cancelEmailByIdAsync.mockImplementationOnce(() => ({
status: 'cancelled',
tag: 'sampleTag',
txId: randomUUID(),
updatedTS: Date.now(),
createdTS: Date.now(),
msgId: randomUUID(),
}));
const result = await notificationServices.cancelNotificationById(1);
expect(result.Status).toBe(NotificationStatus.Cancelled);
});
it('should return unmodified notification in the case of a non cancelled notification', async () => {
_cancelEmailByIdAsync.mockImplementationOnce(() => ({
status: 'completed',
tag: 'sampleTag',
txId: randomUUID(),
updatedTS: Date.now(),
createdTS: Date.now(),
msgId: randomUUID(),
}));
const notif = produceNotificationQueue({ ChesMessageId: randomUUID() });
_notifQueueFindOne.mockImplementationOnce(() => notif);
const result = await notificationServices.cancelNotificationById(1);
expect(result.ChesMessageId).toBe(notif.ChesMessageId);
expect(result.Status).toBe(notif.Status);
});
});
describe('getNotificationById', () => {
it('should return a single notification', async () => {
const result = await notificationServices.getNotificationById(1);
expect(result).toBeDefined();
expect(result.Status).toBeDefined();
expect(result.TemplateId).toBeDefined();
expect(result.Id).toBeDefined();
});
});
Loading

0 comments on commit 078448e

Please sign in to comment.