feat(backend): only one email is sent although more notifications are triggered (#8400)

Co-authored-by: Ulf Gebhardt <ulf.gebhardt@webcraft-media.de>
This commit is contained in:
Moriz Wahl 2025-04-17 19:55:53 +02:00 committed by GitHub
parent aee46552d6
commit de4325cb50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 747 additions and 20 deletions

View File

@ -0,0 +1,716 @@
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 } 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
const driver = getDriver()
const neode = getNeode()
const mentionString =
'<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!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
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 followUserMutation = gql`
mutation ($id: ID!) {
followUser(id: $id) {
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('emails sent for notifications', () => {
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: 'group.member@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',
},
})
authenticatedUser = await groupMember.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'public-group',
userId: 'group-member',
},
})
await mutate({
mutation: followUserMutation,
variables: { id: 'post-author' },
})
})
afterEach(async () => {
await cleanDatabase()
})
describe('handleContentDataOfPost', () => {
describe('post-author posts into group and mentions following group-member', () => {
describe('all email notification settings are true', () => {
beforeEach(async () => {
jest.clearAllMocks()
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`,
groupId: 'public-group',
},
})
})
it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
it('sends 3 notifications', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining([
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'post_in_group',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'followed_user_posted',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'mentioned_in_post',
relatedUser: null,
},
]),
},
errors: undefined,
})
})
})
describe('email notification for mention in post is false', () => {
beforeEach(async () => {
jest.clearAllMocks()
await groupMember.update({ emailNotificationsMention: false })
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`,
groupId: 'public-group',
},
})
})
it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
it('sends 3 notifications', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining([
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'post_in_group',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'followed_user_posted',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'mentioned_in_post',
relatedUser: null,
},
]),
},
errors: undefined,
})
})
})
describe('email notification for mention in post and followed users is false', () => {
beforeEach(async () => {
jest.clearAllMocks()
await groupMember.update({ emailNotificationsMention: false })
await groupMember.update({ emailNotificationsFollowingUsers: false })
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`,
groupId: 'public-group',
},
})
})
it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
it('sends 3 notifications', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining([
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'post_in_group',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'followed_user_posted',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'mentioned_in_post',
relatedUser: null,
},
]),
},
errors: undefined,
})
})
})
describe('all relevant email notifications are false', () => {
beforeEach(async () => {
jest.clearAllMocks()
await groupMember.update({ emailNotificationsMention: false })
await groupMember.update({ emailNotificationsFollowingUsers: false })
await groupMember.update({ emailNotificationsPostInGroup: false })
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`,
groupId: 'public-group',
},
})
})
it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled()
})
it('sends 3 notifications', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining([
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'post_in_group',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'followed_user_posted',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Post',
id: 'post',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.',
},
read: false,
reason: 'mentioned_in_post',
relatedUser: null,
},
]),
},
errors: undefined,
})
})
})
})
})
describe('handleContentDataOfComment', () => {
describe('user comments post and author responds with in a comment and mentions the user', () => {
describe('all email notification settings are true', () => {
beforeEach(async () => {
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`,
groupId: 'public-group',
},
})
authenticatedUser = await groupMember.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
id: 'comment',
content: `Hello, my beloved author.`,
postId: 'post',
},
})
await markAllAsRead()
jest.clearAllMocks()
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
id: 'comment-2',
content: `Hello, ${mentionString}, my beloved follower.`,
postId: 'post',
},
})
})
it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
it('sends 2 notifications', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining([
{
createdAt: expect.any(String),
from: {
__typename: 'Comment',
id: 'comment-2',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.',
},
read: false,
reason: 'commented_on_post',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Comment',
id: 'comment-2',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.',
},
read: false,
reason: 'mentioned_in_comment',
relatedUser: null,
},
]),
},
errors: undefined,
})
})
})
describe('email notification commented on post is false', () => {
beforeEach(async () => {
await groupMember.update({ emailNotificationsCommentOnObservedPost: false })
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`,
groupId: 'public-group',
},
})
authenticatedUser = await groupMember.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
id: 'comment',
content: `Hello, my beloved author.`,
postId: 'post',
},
})
await markAllAsRead()
jest.clearAllMocks()
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
id: 'comment-2',
content: `Hello, ${mentionString}, my beloved follower.`,
postId: 'post',
},
})
})
it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
it('sends 2 notifications', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining([
{
createdAt: expect.any(String),
from: {
__typename: 'Comment',
id: 'comment-2',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.',
},
read: false,
reason: 'commented_on_post',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Comment',
id: 'comment-2',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.',
},
read: false,
reason: 'mentioned_in_comment',
relatedUser: null,
},
]),
},
errors: undefined,
})
})
})
describe('all relevant email notifications are false', () => {
beforeEach(async () => {
await groupMember.update({ emailNotificationsCommentOnObservedPost: false })
await groupMember.update({ emailNotificationsMention: false })
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createPostMutation,
variables: {
id: 'post',
title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`,
groupId: 'public-group',
},
})
authenticatedUser = await groupMember.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
id: 'comment',
content: `Hello, my beloved author.`,
postId: 'post',
},
})
await markAllAsRead()
jest.clearAllMocks()
authenticatedUser = await postAuthor.toJson()
await mutate({
mutation: createCommentMutation,
variables: {
id: 'comment-2',
content: `Hello, ${mentionString}, my beloved follower.`,
postId: 'post',
},
})
})
it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled()
})
it('sends 2 notifications', async () => {
authenticatedUser = await groupMember.toJson()
await expect(
query({
query: notificationQuery,
variables: {
read: false,
},
}),
).resolves.toMatchObject({
data: {
notifications: expect.arrayContaining([
{
createdAt: expect.any(String),
from: {
__typename: 'Comment',
id: 'comment-2',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.',
},
read: false,
reason: 'commented_on_post',
relatedUser: null,
},
{
createdAt: expect.any(String),
from: {
__typename: 'Comment',
id: 'comment-2',
content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.',
},
read: false,
reason: 'mentioned_in_comment',
relatedUser: null,
},
]),
},
errors: undefined,
})
})
})
})
})
})

View File

@ -124,7 +124,7 @@ describe('mentions in groups', () => {
slug: 'post-author',
},
{
email: 'test@example.org',
email: 'post.author@example.org',
password: '1234',
},
)
@ -136,7 +136,7 @@ describe('mentions in groups', () => {
slug: 'group-member',
},
{
email: 'test2@example.org',
email: 'group.member@example.org',
password: '1234',
},
)
@ -148,7 +148,7 @@ describe('mentions in groups', () => {
slug: 'pending-member',
},
{
email: 'test3@example.org',
email: 'pending.member@example.org',
password: '1234',
},
)
@ -160,7 +160,7 @@ describe('mentions in groups', () => {
slug: 'no-member',
},
{
email: 'test4@example.org',
email: 'no.member@example.org',
password: '1234',
},
)
@ -347,8 +347,8 @@ describe('mentions in groups', () => {
})
})
it('sends 3 emails, 2 mentions and 1 post in group', () => {
expect(sendMailMock).toHaveBeenCalledTimes(5)
it('sends 3 emails, one for each user', () => {
expect(sendMailMock).toHaveBeenCalledTimes(3)
})
})
@ -443,8 +443,8 @@ describe('mentions in groups', () => {
})
})
it('sends 2 emails, one mention and one post in group', () => {
expect(sendMailMock).toHaveBeenCalledTimes(2)
it('sends only 1 email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
})
@ -539,8 +539,8 @@ describe('mentions in groups', () => {
})
})
it('sends 2 emails, one mention and one post in group', () => {
expect(sendMailMock).toHaveBeenCalledTimes(2)
it('sends only 1 email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
})
})

View File

@ -40,7 +40,12 @@ const queryNotificationEmails = async (context, notificationUserIds) => {
}
}
const publishNotifications = async (context, promises, emailNotificationSetting: string) => {
const publishNotifications = async (
context,
promises,
emailNotificationSetting: string,
emailsSent: string[] = [],
): Promise<string[]> => {
let notifications = await Promise.all(promises)
notifications = notifications.flat()
const notificationsEmailAddresses = await queryNotificationEmails(
@ -51,7 +56,8 @@ const publishNotifications = async (context, promises, emailNotificationSetting:
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
if (
(notificationAdded.to[emailNotificationSetting] ?? true) &&
!isUserOnline(notificationAdded.to)
!isUserOnline(notificationAdded.to) &&
!emailsSent.includes(notificationsEmailAddresses[index].email)
) {
sendMail(
notificationTemplate({
@ -59,8 +65,10 @@ const publishNotifications = async (context, promises, emailNotificationSetting:
variables: { notification: notificationAdded },
}),
)
emailsSent.push(notificationsEmailAddresses[index].email)
}
})
return emailsSent
}
const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => {
@ -120,20 +128,24 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo
const idsOfUsers = extractMentionedUsers(args.content)
const post = await resolve(root, args, context, resolveInfo)
if (post) {
await publishNotifications(
const sentEmails: string[] = await publishNotifications(
context,
[notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)],
'emailNotificationsMention',
)
await publishNotifications(
context,
[notifyFollowingUsers(post.id, groupId, context)],
'emailNotificationsFollowingUsers',
sentEmails.concat(
await publishNotifications(
context,
[notifyFollowingUsers(post.id, groupId, context)],
'emailNotificationsFollowingUsers',
sentEmails,
),
)
await publishNotifications(
context,
[notifyGroupMembersOfNewPost(post.id, groupId, context)],
'emailNotificationsPostInGroup',
sentEmails,
)
}
return post
@ -145,7 +157,7 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
const comment = await resolve(root, args, context, resolveInfo)
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id)
await publishNotifications(
const sentEmails: string[] = await publishNotifications(
context,
[
notifyUsersOfMention(
@ -158,13 +170,12 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
],
'emailNotificationsMention',
)
await publishNotifications(
context,
[notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context)],
'emailNotificationsCommentOnObservedPost',
sentEmails,
)
return comment
}