diff --git a/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts index a46de2830..ad336596d 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts @@ -118,7 +118,7 @@ afterAll(async () => { }) describe('notify group members of new posts in group', () => { - beforeAll(async () => { + beforeEach(async () => { postAuthor = await Factory.build( 'user', { @@ -193,8 +193,12 @@ describe('notify group members of new posts in group', () => { }) }) + afterEach(async () => { + await cleanDatabase() + }) + describe('group owner posts in group', () => { - beforeAll(async () => { + beforeEach(async () => { jest.clearAllMocks() authenticatedUser = await groupMember.toJson() await markAllAsRead() @@ -275,29 +279,15 @@ describe('notify group members of new posts in group', () => { }) describe('group member mutes group', () => { - it('sets the muted status correctly', async () => { + beforeEach(async () => { authenticatedUser = await groupMember.toJson() - await expect( - mutate({ - mutation: muteGroupMutation, - variables: { - groupId: 'g-1', - }, - }), - ).resolves.toMatchObject({ - data: { - muteGroup: { - isMutedByMe: true, - }, + await mutate({ + mutation: muteGroupMutation, + variables: { + groupId: 'g-1', }, - errors: undefined, }) - }) - - it('sends NO notification when another post is posted', async () => { jest.clearAllMocks() - authenticatedUser = await groupMember.toJson() - await markAllAsRead() authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, @@ -308,7 +298,9 @@ describe('notify group members of new posts in group', () => { groupId: 'g-1', }, }) - authenticatedUser = await groupMember.toJson() + }) + + it('sends NO notification when another post is posted', async () => { await expect( query({ query: notificationQuery, @@ -329,30 +321,18 @@ describe('notify group members of new posts in group', () => { }) describe('group member unmutes group again but disables email', () => { - beforeAll(async () => { + beforeEach(async () => { + authenticatedUser = await groupMember.toJson() + await mutate({ + mutation: unmuteGroupMutation, + variables: { + groupId: 'g-1', + }, + }) jest.clearAllMocks() await groupMember.update({ emailNotificationsPostInGroup: false }) }) - it('sets the muted status correctly', async () => { - authenticatedUser = await groupMember.toJson() - await expect( - mutate({ - mutation: unmuteGroupMutation, - variables: { - groupId: 'g-1', - }, - }), - ).resolves.toMatchObject({ - data: { - unmuteGroup: { - isMutedByMe: false, - }, - }, - errors: undefined, - }) - }) - it('sends notification when another post is posted', async () => { authenticatedUser = await groupMember.toJson() await markAllAsRead() @@ -396,5 +376,85 @@ describe('notify group members of new posts in group', () => { }) }) }) + + describe('group member blocks author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'blocked') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) + + describe('group member mutes author', () => { + beforeEach(async () => { + await groupMember.relateTo(postAuthor, 'muted') + authenticatedUser = await groupMember.toJson() + await markAllAsRead() + jest.clearAllMocks() + authenticatedUser = await postAuthor.toJson() + await mutate({ + mutation: createPostMutation, + variables: { + id: 'post-1', + title: 'This is another post in the group', + content: 'This is the content of another post in the group', + groupId: 'g-1', + }, + }) + }) + + it('sends no notification to the user', async () => { + authenticatedUser = await groupMember.toJson() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [], + }, + errors: undefined, + }) + }) + + it('sends NO email', () => { + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index 90888cf8b..c58acc5e2 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -294,6 +294,25 @@ describe('notifications', () => { ).resolves.toEqual(expected) }) }) + + describe('if I have muted the comment author', () => { + it('sends me no notification', async () => { + await notifiedUser.relateTo(commentAuthor, 'muted') + await createCommentOnPostAction() + const expected = expect.objectContaining({ + data: { notifications: [] }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + }) }) describe('commenter is me', () => { @@ -581,6 +600,48 @@ describe('notifications', () => { expect(pubsubSpy).not.toHaveBeenCalled() }) }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + }) + + it('sends me a notification', async () => { + await createPostAction() + const expected = expect.objectContaining({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Post', + content: + 'Hey @al-capone how do you do?', + id: 'p47', + }, + read: false, + reason: 'mentioned_in_post', + relatedUser: null, + }, + ], + }, + }) + + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toEqual(expected) + }) + + it('publishes `NOTIFICATION_ADDED`', async () => { + await createPostAction() + expect(pubsubSpy).toHaveBeenCalled() + }) + }) }) describe('mentions me in a comment', () => { @@ -736,6 +797,72 @@ describe('notifications', () => { expect(pubsubSpy).toHaveBeenCalledTimes(1) }) }) + + describe('but the author of the post muted me', () => { + beforeEach(async () => { + await postAuthor.relateTo(notifiedUser, 'muted') + commentContent = + 'One mention about me with @al-capone.' + commentAuthor = await neode.create( + 'User', + { + id: 'commentAuthor', + name: 'Mrs Comment', + slug: 'mrs-comment', + }, + { + email: 'comment-author@example.org', + password: '1234', + }, + ) + }) + + it('sends me a notification', async () => { + await createCommentOnPostAction() + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + createdAt: expect.any(String), + from: { + __typename: 'Comment', + content: + 'One mention about me with @al-capone.', + id: 'c47', + }, + read: false, + reason: 'mentioned_in_comment', + relatedUser: null, + }, + ], + }, + errors: undefined, + }) + }) + + it('publishes `NOTIFICATION_ADDED` to authenticated user and me', async () => { + await createCommentOnPostAction() + expect(pubsubSpy).toHaveBeenCalledWith( + 'NOTIFICATION_ADDED', + expect.objectContaining({ + notificationAdded: expect.objectContaining({ + reason: 'commented_on_post', + to: expect.objectContaining({ + id: 'postAuthor', // that's expected, it's not me but the post author + }), + }), + }), + ) + expect(pubsubSpy).toHaveBeenCalledTimes(2) + }) + }) }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index 4fb8cba93..a8d95b284 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -245,6 +245,8 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User) WHERE NOT membership.role = 'pending' AND NOT (user)-[:MUTED]->(group) + AND NOT (user)-[:MUTED]->(author) + AND NOT (user)-[:BLOCKED]-(author) AND NOT user.id = $userId WITH post, author, user MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) @@ -360,7 +362,10 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { case 'mentioned_in_post': { mentionedCypher = ` MATCH (post: Post { id: $id })<-[:WROTE]-(author: User) - MATCH (user: User) WHERE user.id in $idsOfUsers AND NOT (user)-[:BLOCKED]-(author) + MATCH (user: User) + WHERE user.id in $idsOfUsers + AND NOT (user)-[:BLOCKED]-(author) + AND NOT (user)-[:MUTED]->(author) OPTIONAL MATCH (post)-[:IN]->(group:Group) OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) WITH post, author, user, group WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] @@ -376,6 +381,8 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { WHERE user.id in $idsOfUsers AND NOT (user)-[:BLOCKED]-(commenter) AND NOT (user)-[:BLOCKED]-(postAuthor) + AND NOT (user)-[:MUTED]->(commenter) + AND NOT (user)-[:MUTED]->(postAuthor) OPTIONAL MATCH (post)-[:IN]->(group:Group) OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) WITH comment, user, group WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] @@ -422,7 +429,9 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { const notificationTransactionResponse = await transaction.run( ` MATCH (observingUser:User)-[:OBSERVES { active: true }]->(post:Post)<-[:COMMENTS]-(comment:Comment { id: $commentId })<-[:WROTE]-(commenter:User) - WHERE NOT (observingUser)-[:BLOCKED]-(commenter) AND NOT observingUser.id = $userId + WHERE NOT (observingUser)-[:BLOCKED]-(commenter) + AND NOT (observingUser)-[:MUTED]->(commenter) + AND NOT observingUser.id = $userId WITH observingUser, post, comment, commenter MATCH (postAuthor:User)-[:WROTE]->(post) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser)