Skip to content

Commit

Permalink
feat(api): inbox - update the notification status endpoint (novuhq#5836)
Browse files Browse the repository at this point in the history
* feat(api): inbox - update the notification status endpoint
  • Loading branch information
LetItRock authored Jul 7, 2024
1 parent 688eee3 commit 87fa90f
Show file tree
Hide file tree
Showing 27 changed files with 1,659 additions and 13 deletions.
4 changes: 3 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,9 @@
"tailwindcss",
"focusable",
"textareas",
"frameworkterminal"
"frameworkterminal",
"unarchived",
"Unarchived"
],
"flagWords": [],
"patterns": [
Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/app/inbox/dtos/action-type-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IsDefined, IsEnum } from 'class-validator';
import { ButtonTypeEnum } from '@novu/shared';

export class ActionTypeRequestDto {
@IsEnum(ButtonTypeEnum)
@IsDefined()
readonly actionType: ButtonTypeEnum;
}
9 changes: 9 additions & 0 deletions apps/api/src/app/inbox/e2e/get-notifications-count.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ describe('Get Notifications Count - /inbox/notifications/count (GET)', async ()
await session.awaitRunningJobs(templateToTrigger._id);
};

it('should throw exception when filtering for unread and archived notifications', async function () {
await triggerEvent(template);

const { body, status } = await getNotificationsCount({ read: false, archived: true });

expect(status).to.equal(400);
expect(body.message).to.equal('Filtering for unread and archived notifications is not supported.');
});

it('should return all notifications count', async function () {
const count = 4;
await triggerEvent(template, count);
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/app/inbox/e2e/get-notifications.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ describe('Get Notifications - /inbox/notifications (GET)', async () => {
expect(body.message[0]).to.equal('The after cursor must be a valid MongoDB ObjectId');
});

it('should throw exception when filtering for unread and archived notifications', async function () {
await triggerEvent(template);

const { body, status } = await getNotifications({ limit: 1, read: false, archived: true });

expect(status).to.equal(400);
expect(body.message).to.equal('Filtering for unread and archived notifications is not supported.');
});

it('should include fields from message entity', async function () {
await triggerEvent(template);

Expand Down
213 changes: 213 additions & 0 deletions apps/api/src/app/inbox/e2e/mark-notification-as.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { expect } from 'chai';
import { UserSession } from '@novu/testing';
import {
MessageEntity,
MessageRepository,
NotificationTemplateEntity,
SubscriberEntity,
SubscriberRepository,
} from '@novu/dal';
import {
StepTypeEnum,
ChannelCTATypeEnum,
TemplateVariableTypeEnum,
ActorTypeEnum,
SystemAvatarIconEnum,
ButtonTypeEnum,
} from '@novu/shared';

import { mapToDto } from '../utils/notification-mapper';

describe('Mark Notification As - /inbox/notifications/:id/mark-as-{status} (PATCH)', async () => {
let session: UserSession;
let template: NotificationTemplateEntity;
let subscriber: SubscriberEntity | null;
let message: MessageEntity;
const messageRepository = new MessageRepository();
const subscriberRepository = new SubscriberRepository();

const updateNotification = async ({
id,
status,
}: {
id: string;
status: 'read' | 'unread' | 'archived' | 'unarchived';
}) => {
return await session.testAgent
.patch(`/v1/inbox/notifications/${id}/mark-as-${status}`)
.set('Authorization', `Bearer ${session.subscriberToken}`)
.send();
};

const triggerEvent = async (templateToTrigger: NotificationTemplateEntity, times = 1) => {
const promises: Array<Promise<unknown>> = [];
for (let i = 0; i < times; i++) {
promises.push(session.triggerEvent(templateToTrigger.triggers[0].identifier, session.subscriberId));
}

await Promise.all(promises);
await session.awaitRunningJobs(templateToTrigger._id);
};

const removeUndefinedDeep = (obj) => {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return obj;

const newObj = {};
for (const key in obj) {
if (obj[key] !== undefined) {
newObj[key] = removeUndefinedDeep(obj[key]);
}
}

return newObj;
};

beforeEach(async () => {
session = new UserSession();
await session.initialize();

subscriber = await subscriberRepository.findBySubscriberId(session.environment._id, session.subscriberId);
template = await session.createTemplate({
noFeedId: true,
steps: [
{
type: StepTypeEnum.IN_APP,
content: 'Test content for <b>{{firstName}}</b>',
cta: {
type: ChannelCTATypeEnum.REDIRECT,
data: {
url: '',
},
action: {
buttons: [
{ type: ButtonTypeEnum.PRIMARY, content: '' },
{ type: ButtonTypeEnum.SECONDARY, content: '' },
],
},
},
variables: [
{
defaultValue: '',
name: 'firstName',
required: false,
type: TemplateVariableTypeEnum.STRING,
},
],
actor: {
type: ActorTypeEnum.SYSTEM_ICON,
data: SystemAvatarIconEnum.WARNING,
},
},
],
});
await triggerEvent(template);
message = (await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
})) as MessageEntity;
});

it('should throw bad request error when the notification id is not mongo id', async function () {
const id = 'fake';
const { body, status } = await updateNotification({ id, status: 'read' });

expect(status).to.equal(400);
expect(body.message[0]).to.equal(`notificationId must be a mongodb id`);
});

it("should throw not found error when the message doesn't exist", async function () {
const id = '666c0dfa0b55d0f06f4aaa6c';
const { body, status } = await updateNotification({ id, status: 'read' });

expect(status).to.equal(404);
expect(body.message).to.equal(`Notification with id: ${id} is not found.`);
});

it('should update the read status', async function () {
const { body, status } = await updateNotification({ id: message._id, status: 'read' });
const updatedMessage = (await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
})) as MessageEntity;

expect(status).to.equal(200);
expect(body.data).to.deep.equal(removeUndefinedDeep(mapToDto(updatedMessage)));
expect(updatedMessage.seen).to.be.true;
expect(updatedMessage.lastSeenDate).not.to.be.undefined;
expect(body.data.read).to.be.true;
expect(body.data.readAt).not.to.be.undefined;
expect(body.data.archived).to.be.false;
expect(body.data.archivedAt).to.be.undefined;
});

it('should update the unread status', async function () {
const now = new Date();
await messageRepository.update(
{ _id: message._id, _environmentId: message._environmentId },
{ $set: { seen: true, lastSeenDate: now, read: true, lastReadDate: now, archived: true, archivedAt: now } }
);

const { body, status } = await updateNotification({ id: message._id, status: 'unread' });

const updatedMessage = (await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
})) as MessageEntity;

expect(status).to.equal(200);
expect(body.data).to.deep.equal(removeUndefinedDeep(mapToDto(updatedMessage)));
expect(updatedMessage.seen).to.be.true;
expect(updatedMessage.lastSeenDate).not.to.be.undefined;
expect(body.data.read).to.be.false;
expect(body.data.readAt).to.be.null;
expect(body.data.archived).to.be.false;
expect(body.data.archivedAt).to.be.null;
});

it('should update the archived status', async function () {
const { body, status } = await updateNotification({ id: message._id, status: 'archived' });

const updatedMessage = (await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
})) as MessageEntity;

expect(status).to.equal(200);
expect(body.data).to.deep.equal(removeUndefinedDeep(mapToDto(updatedMessage)));
expect(updatedMessage.seen).to.be.true;
expect(updatedMessage.lastSeenDate).not.to.be.undefined;
expect(body.data.read).to.be.true;
expect(body.data.readAt).not.to.be.undefined;
expect(body.data.archived).to.be.true;
expect(body.data.archivedAt).not.to.be.undefined;
});

it('should update the unarchived status', async function () {
const now = new Date();
await messageRepository.update(
{ _id: message._id, _environmentId: message._environmentId },
{ $set: { seen: true, lastSeenDate: now, read: true, lastReadDate: now, archived: true, archivedAt: now } }
);

const { body, status } = await updateNotification({ id: message._id, status: 'unarchived' });

const updatedMessage = (await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
})) as MessageEntity;

expect(status).to.equal(200);
expect(body.data).to.deep.equal(removeUndefinedDeep(mapToDto(updatedMessage)));
expect(updatedMessage.seen).to.be.true;
expect(updatedMessage.lastSeenDate).not.to.be.undefined;
expect(body.data.read).to.be.true;
expect(body.data.readAt).not.to.be.undefined;
expect(body.data.archived).to.be.false;
expect(body.data.archivedAt).to.be.null;
});
});
Loading

0 comments on commit 87fa90f

Please sign in to comment.