diff --git a/backend/src/middleware/notifications/followed-users.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts
similarity index 100%
rename from backend/src/middleware/notifications/followed-users.spec.ts
rename to backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts
diff --git a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts
new file mode 100644
index 000000000..575a2e86d
--- /dev/null
+++ b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts
@@ -0,0 +1,789 @@
+import { createTestClient } from 'apollo-server-testing'
+import gql from 'graphql-tag'
+
+import Factory, { cleanDatabase } from '@db/factories'
+import { getNeode, getDriver } from '@db/neo4j'
+import {
+ createGroupMutation,
+ joinGroupMutation,
+ changeGroupMemberRoleMutation,
+} from '@graphql/groups'
+import CONFIG from '@src/config'
+import createServer from '@src/server'
+
+CONFIG.CATEGORIES_ACTIVE = false
+
+const sendMailMock = jest.fn()
+jest.mock('../helpers/email/sendMail', () => ({
+ sendMail: () => sendMailMock(),
+}))
+
+let server, query, mutate, authenticatedUser
+
+let postAuthor, groupMember, pendingMember, noMember
+
+const driver = getDriver()
+const neode = getNeode()
+
+const mentionString = `
+ @no-meber
+ @pending-member
+ @group-member.
+`
+
+const createPostMutation = gql`
+ mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) {
+ CreatePost(id: $id, title: $title, content: $content, groupId: $groupId) {
+ id
+ title
+ content
+ }
+ }
+`
+
+const createCommentMutation = gql`
+ mutation ($id: ID, $postId: ID!, $commentContent: String!) {
+ CreateComment(id: $id, postId: $postId, content: $commentContent) {
+ id
+ content
+ }
+ }
+`
+
+const notificationQuery = gql`
+ query ($read: Boolean) {
+ notifications(read: $read, orderBy: updatedAt_desc) {
+ read
+ reason
+ createdAt
+ relatedUser {
+ id
+ }
+ from {
+ __typename
+ ... on Post {
+ id
+ content
+ }
+ ... on Comment {
+ id
+ content
+ }
+ ... on Group {
+ id
+ }
+ }
+ }
+ }
+`
+
+const markAllAsRead = async () =>
+ mutate({
+ mutation: gql`
+ mutation {
+ markAllAsRead {
+ id
+ }
+ }
+ `,
+ })
+
+beforeAll(async () => {
+ await cleanDatabase()
+
+ const createServerResult = createServer({
+ context: () => {
+ return {
+ user: authenticatedUser,
+ neode,
+ driver,
+ cypherParams: {
+ currentUserId: authenticatedUser ? authenticatedUser.id : null,
+ },
+ }
+ },
+ })
+ server = createServerResult.server
+ const createTestClientResult = createTestClient(server)
+ query = createTestClientResult.query
+ mutate = createTestClientResult.mutate
+})
+
+afterAll(async () => {
+ await cleanDatabase()
+ driver.close()
+})
+
+describe('mentions in groups', () => {
+ beforeEach(async () => {
+ postAuthor = await Factory.build(
+ 'user',
+ {
+ id: 'post-author',
+ name: 'Post Author',
+ slug: 'post-author',
+ },
+ {
+ email: 'test@example.org',
+ password: '1234',
+ },
+ )
+ groupMember = await Factory.build(
+ 'user',
+ {
+ id: 'group-member',
+ name: 'Group Member',
+ slug: 'group-member',
+ },
+ {
+ email: 'test2@example.org',
+ password: '1234',
+ },
+ )
+ pendingMember = await Factory.build(
+ 'user',
+ {
+ id: 'pending-member',
+ name: 'Pending Member',
+ slug: 'pending-member',
+ },
+ {
+ email: 'test3@example.org',
+ password: '1234',
+ },
+ )
+ noMember = await Factory.build(
+ 'user',
+ {
+ id: 'no-member',
+ name: 'No Member',
+ slug: 'no-member',
+ },
+ {
+ email: 'test4@example.org',
+ password: '1234',
+ },
+ )
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: createGroupMutation(),
+ variables: {
+ id: 'public-group',
+ name: 'A public group',
+ description: 'A public group to test the notifications of mentions',
+ groupType: 'public',
+ actionRadius: 'national',
+ },
+ })
+ await mutate({
+ mutation: createGroupMutation(),
+ variables: {
+ id: 'closed-group',
+ name: 'A closed group',
+ description: 'A closed group to test the notifications of mentions',
+ groupType: 'closed',
+ actionRadius: 'national',
+ },
+ })
+ await mutate({
+ mutation: createGroupMutation(),
+ variables: {
+ id: 'hidden-group',
+ name: 'A hidden group',
+ description: 'A hidden group to test the notifications of mentions',
+ groupType: 'hidden',
+ actionRadius: 'national',
+ },
+ })
+ authenticatedUser = await groupMember.toJson()
+ await mutate({
+ mutation: joinGroupMutation(),
+ variables: {
+ groupId: 'public-group',
+ userId: 'group-member',
+ },
+ })
+ await mutate({
+ mutation: joinGroupMutation(),
+ variables: {
+ groupId: 'closed-group',
+ userId: 'group-member',
+ },
+ })
+ await mutate({
+ mutation: joinGroupMutation(),
+ variables: {
+ groupId: 'hidden-group',
+ userId: 'group-member',
+ },
+ })
+ authenticatedUser = await pendingMember.toJson()
+ await mutate({
+ mutation: joinGroupMutation(),
+ variables: {
+ groupId: 'public-group',
+ userId: 'pending-member',
+ },
+ })
+ await mutate({
+ mutation: joinGroupMutation(),
+ variables: {
+ groupId: 'closed-group',
+ userId: 'pending-member',
+ },
+ })
+ await mutate({
+ mutation: joinGroupMutation(),
+ variables: {
+ groupId: 'hidden-group',
+ userId: 'pending-member',
+ },
+ })
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: changeGroupMemberRoleMutation(),
+ variables: {
+ groupId: 'closed-group',
+ userId: 'group-member',
+ roleInGroup: 'usual',
+ },
+ })
+ await mutate({
+ mutation: changeGroupMemberRoleMutation(),
+ variables: {
+ groupId: 'hidden-group',
+ userId: 'group-member',
+ roleInGroup: 'usual',
+ },
+ })
+ authenticatedUser = await groupMember.toJson()
+ await markAllAsRead()
+ })
+
+ afterEach(async () => {
+ await cleanDatabase()
+ })
+
+ describe('post in public group', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks()
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: createPostMutation,
+ variables: {
+ id: 'public-post',
+ title: 'This is the post in the public group',
+ content: `Hey ${mentionString}! Please read this`,
+ groupId: 'public-group',
+ },
+ })
+ })
+
+ it('sends a notification to the no member', async () => {
+ authenticatedUser = await noMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Post',
+ id: 'public-post',
+ },
+ read: false,
+ reason: 'mentioned_in_post',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends a notification to the group member', async () => {
+ authenticatedUser = await groupMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Post',
+ id: 'public-post',
+ },
+ read: false,
+ reason: 'post_in_group',
+ },
+ {
+ from: {
+ __typename: 'Post',
+ id: 'public-post',
+ },
+ read: false,
+ reason: 'mentioned_in_post',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends 3 emails, 2 mentions and 1 post in group', () => {
+ expect(sendMailMock).toHaveBeenCalledTimes(5)
+ })
+ })
+
+ describe('post in closed group', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks()
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: createPostMutation,
+ variables: {
+ id: 'closed-post',
+ title: 'This is the post in the closed group',
+ content: `Hey members ${mentionString}! Please read this`,
+ groupId: 'closed-group',
+ },
+ })
+ })
+
+ it('sends NO notification to the no member', async () => {
+ authenticatedUser = await noMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends NO notification to the pending member', async () => {
+ authenticatedUser = await pendingMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends a notification to the group member', async () => {
+ authenticatedUser = await groupMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Post',
+ id: 'closed-post',
+ },
+ read: false,
+ reason: 'post_in_group',
+ },
+ {
+ from: {
+ __typename: 'Post',
+ id: 'closed-post',
+ },
+ read: false,
+ reason: 'mentioned_in_post',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends 2 emails, one mention and one post in group', () => {
+ expect(sendMailMock).toHaveBeenCalledTimes(2)
+ })
+ })
+
+ describe('post in hidden group', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks()
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: createPostMutation,
+ variables: {
+ id: 'hidden-post',
+ title: 'This is the post in the hidden group',
+ content: `Hey hiders ${mentionString}! Please read this`,
+ groupId: 'hidden-group',
+ },
+ })
+ })
+
+ it('sends NO notification to the no member', async () => {
+ authenticatedUser = await noMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends NO notification to the pending member', async () => {
+ authenticatedUser = await pendingMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends a notification to the group member', async () => {
+ authenticatedUser = await groupMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Post',
+ id: 'hidden-post',
+ },
+ read: false,
+ reason: 'post_in_group',
+ },
+ {
+ from: {
+ __typename: 'Post',
+ id: 'hidden-post',
+ },
+ read: false,
+ reason: 'mentioned_in_post',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends 2 emails, one mention and one post in group', () => {
+ expect(sendMailMock).toHaveBeenCalledTimes(2)
+ })
+ })
+
+ describe('comments on group posts', () => {
+ describe('public group', () => {
+ beforeEach(async () => {
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: createPostMutation,
+ variables: {
+ id: 'public-post',
+ title: 'This is the post in the public group',
+ content: `Some public content`,
+ groupId: 'public-group',
+ },
+ })
+ authenticatedUser = await groupMember.toJson()
+ await markAllAsRead()
+ authenticatedUser = await postAuthor.toJson()
+ jest.clearAllMocks()
+ await mutate({
+ mutation: createCommentMutation,
+ variables: {
+ id: 'public-comment',
+ postId: 'public-post',
+ commentContent: `Hey everyone ${mentionString}! Please read this`,
+ },
+ })
+ })
+
+ it('sends a notification to the no member', async () => {
+ authenticatedUser = await noMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Comment',
+ id: 'public-comment',
+ },
+ read: false,
+ reason: 'mentioned_in_comment',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends a notification to the group member', async () => {
+ authenticatedUser = await groupMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Comment',
+ id: 'public-comment',
+ },
+ read: false,
+ reason: 'mentioned_in_comment',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends 2 emails', () => {
+ expect(sendMailMock).toHaveBeenCalledTimes(3)
+ })
+ })
+
+ describe('closed group', () => {
+ beforeEach(async () => {
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: createPostMutation,
+ variables: {
+ id: 'closed-post',
+ title: 'This is the post in the closed group',
+ content: `Some closed content`,
+ groupId: 'closed-group',
+ },
+ })
+ authenticatedUser = await groupMember.toJson()
+ await markAllAsRead()
+ authenticatedUser = await postAuthor.toJson()
+ jest.clearAllMocks()
+ await mutate({
+ mutation: createCommentMutation,
+ variables: {
+ id: 'closed-comment',
+ postId: 'closed-post',
+ commentContent: `Hey members ${mentionString}! Please read this`,
+ },
+ })
+ })
+
+ it('sends NO notification to the no member', async () => {
+ authenticatedUser = await noMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends NO notification to the pending member', async () => {
+ authenticatedUser = await pendingMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends a notification to the group member', async () => {
+ authenticatedUser = await groupMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Comment',
+ id: 'closed-comment',
+ },
+ read: false,
+ reason: 'mentioned_in_comment',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends 1 email', () => {
+ expect(sendMailMock).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('hidden group', () => {
+ beforeEach(async () => {
+ authenticatedUser = await postAuthor.toJson()
+ await mutate({
+ mutation: createPostMutation,
+ variables: {
+ id: 'hidden-post',
+ title: 'This is the post in the hidden group',
+ content: `Some hidden content`,
+ groupId: 'hidden-group',
+ },
+ })
+ authenticatedUser = await groupMember.toJson()
+ await markAllAsRead()
+ authenticatedUser = await postAuthor.toJson()
+ jest.clearAllMocks()
+ await mutate({
+ mutation: createCommentMutation,
+ variables: {
+ id: 'hidden-comment',
+ postId: 'hidden-post',
+ commentContent: `Hey hiders ${mentionString}! Please read this`,
+ },
+ })
+ })
+
+ it('sends NO notification to the no member', async () => {
+ authenticatedUser = await noMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends NO notification to the pending member', async () => {
+ authenticatedUser = await pendingMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends a notification to the group member', async () => {
+ authenticatedUser = await groupMember.toJson()
+ await expect(
+ query({
+ query: notificationQuery,
+ variables: {
+ read: false,
+ },
+ }),
+ ).resolves.toMatchObject({
+ data: {
+ notifications: [
+ {
+ from: {
+ __typename: 'Comment',
+ id: 'hidden-comment',
+ },
+ read: false,
+ reason: 'mentioned_in_comment',
+ },
+ ],
+ },
+ errors: undefined,
+ })
+ })
+
+ it('sends 1 email', () => {
+ expect(sendMailMock).toHaveBeenCalledTimes(1)
+ })
+ })
+ })
+})
diff --git a/backend/src/middleware/notifications/observing-posts.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts
similarity index 100%
rename from backend/src/middleware/notifications/observing-posts.spec.ts
rename to backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts
diff --git a/backend/src/middleware/notifications/posts-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts
similarity index 100%
rename from backend/src/middleware/notifications/posts-in-groups.spec.ts
rename to backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts
diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts
index f70e9fc89..27216988f 100644
--- a/backend/src/middleware/notifications/notificationsMiddleware.ts
+++ b/backend/src/middleware/notifications/notificationsMiddleware.ts
@@ -349,9 +349,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)
+ 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']
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH post AS resource, notification, user
`
@@ -361,9 +362,12 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
mentionedCypher = `
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(commenter: User)
MATCH (user: User)
- WHERE user.id in $idsOfUsers
- AND NOT (user)-[:BLOCKED]-(commenter)
- AND NOT (user)-[:BLOCKED]-(postAuthor)
+ WHERE user.id in $idsOfUsers
+ AND NOT (user)-[:BLOCKED]-(commenter)
+ AND NOT (user)-[:BLOCKED]-(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']
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH comment AS resource, notification, user
`