mentiioned users in posts and comments of groups (#8392)

This commit is contained in:
Moriz Wahl 2025-04-16 18:04:31 +02:00 committed by GitHub
parent 9440ad5cc3
commit 727713ac1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 799 additions and 6 deletions

View File

@ -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 = `
<a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member">@no-meber</a>
<a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member">@pending-member</a>
<a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member">@group-member</a>.
`
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)
})
})
})
})

View File

@ -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
`