fix(backend): fix notification emails with different name (#8419)

* fix diffent name notifications

We had emails sent with incorrect names.
This PR combines the query for the email with the user the notification
is sent to since the notification in database was correct.
The underlying problem is the unstable order in which the database can
return values. The results of the two queries were matched by id since
it was assumed that they always return the same order of elements.

lint fixes

fix typo

fix factory

fix tests

* fix tests accoridng to review

also test for the right amount of emails in every test
This commit is contained in:
Ulf Gebhardt 2025-04-24 00:58:53 +02:00 committed by GitHub
parent 5883818b91
commit 649491f7cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 355 additions and 180 deletions

View File

@ -16,20 +16,21 @@ import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock = jest.fn() const sendMailMock: (notification) => void = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({ jest.mock('@middleware/helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(), sendMail: (notification) => sendMailMock(notification),
})) }))
let server, query, mutate, authenticatedUser let server, query, mutate, authenticatedUser, emaillessMember
let postAuthor, groupMember let postAuthor, groupMember
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
const mentionString = const mentionString = `
'<a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member">@group-member</a>' <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member">@group-member</a>
<a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member">@email-less-member</a>`
const createPostMutation = gql` const createPostMutation = gql`
mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) {
@ -148,6 +149,11 @@ describe('emails sent for notifications', () => {
password: '1234', password: '1234',
}, },
) )
emaillessMember = await neode.create('User', {
id: 'email-less-member',
name: 'Email-less Member',
slug: 'email-less-member',
})
authenticatedUser = await postAuthor.toJson() authenticatedUser = await postAuthor.toJson()
await mutate({ await mutate({
mutation: createGroupMutation(), mutation: createGroupMutation(),
@ -171,6 +177,18 @@ describe('emails sent for notifications', () => {
mutation: followUserMutation, mutation: followUserMutation,
variables: { id: 'post-author' }, variables: { id: 'post-author' },
}) })
authenticatedUser = await emaillessMember.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'public-group',
userId: 'group-member',
},
})
await mutate({
mutation: followUserMutation,
variables: { id: 'post-author' },
})
}) })
afterEach(async () => { afterEach(async () => {
@ -188,7 +206,7 @@ describe('emails sent for notifications', () => {
variables: { variables: {
id: 'post', id: 'post',
title: 'This is the post', title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
groupId: 'public-group', groupId: 'public-group',
}, },
}) })
@ -213,7 +231,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'post_in_group', reason: 'post_in_group',
@ -225,7 +243,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'followed_user_posted', reason: 'followed_user_posted',
@ -237,7 +255,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
@ -260,7 +278,7 @@ describe('emails sent for notifications', () => {
variables: { variables: {
id: 'post', id: 'post',
title: 'This is the post', title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
groupId: 'public-group', groupId: 'public-group',
}, },
}) })
@ -285,7 +303,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'post_in_group', reason: 'post_in_group',
@ -297,7 +315,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'followed_user_posted', reason: 'followed_user_posted',
@ -309,7 +327,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
@ -333,7 +351,7 @@ describe('emails sent for notifications', () => {
variables: { variables: {
id: 'post', id: 'post',
title: 'This is the post', title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
groupId: 'public-group', groupId: 'public-group',
}, },
}) })
@ -358,7 +376,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'post_in_group', reason: 'post_in_group',
@ -370,7 +388,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'followed_user_posted', reason: 'followed_user_posted',
@ -382,7 +400,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
@ -407,7 +425,7 @@ describe('emails sent for notifications', () => {
variables: { variables: {
id: 'post', id: 'post',
title: 'This is the post', title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
groupId: 'public-group', groupId: 'public-group',
}, },
}) })
@ -432,7 +450,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'post_in_group', reason: 'post_in_group',
@ -444,7 +462,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'followed_user_posted', reason: 'followed_user_posted',
@ -456,7 +474,7 @@ describe('emails sent for notifications', () => {
__typename: 'Post', __typename: 'Post',
id: 'post', id: 'post',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my trusty follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
@ -481,7 +499,7 @@ describe('emails sent for notifications', () => {
variables: { variables: {
id: 'post', id: 'post',
title: 'This is the post', title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
groupId: 'public-group', groupId: 'public-group',
}, },
}) })
@ -501,7 +519,7 @@ describe('emails sent for notifications', () => {
mutation: createCommentMutation, mutation: createCommentMutation,
variables: { variables: {
id: 'comment-2', id: 'comment-2',
content: `Hello, ${mentionString}, my beloved follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
postId: 'post', postId: 'post',
}, },
}) })
@ -529,7 +547,7 @@ describe('emails sent for notifications', () => {
__typename: 'Comment', __typename: 'Comment',
id: 'comment-2', id: 'comment-2',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'commented_on_post', reason: 'commented_on_post',
@ -541,7 +559,7 @@ describe('emails sent for notifications', () => {
__typename: 'Comment', __typename: 'Comment',
id: 'comment-2', id: 'comment-2',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'mentioned_in_comment', reason: 'mentioned_in_comment',
@ -563,7 +581,7 @@ describe('emails sent for notifications', () => {
variables: { variables: {
id: 'post', id: 'post',
title: 'This is the post', title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
groupId: 'public-group', groupId: 'public-group',
}, },
}) })
@ -583,7 +601,7 @@ describe('emails sent for notifications', () => {
mutation: createCommentMutation, mutation: createCommentMutation,
variables: { variables: {
id: 'comment-2', id: 'comment-2',
content: `Hello, ${mentionString}, my beloved follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
postId: 'post', postId: 'post',
}, },
}) })
@ -611,7 +629,7 @@ describe('emails sent for notifications', () => {
__typename: 'Comment', __typename: 'Comment',
id: 'comment-2', id: 'comment-2',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'commented_on_post', reason: 'commented_on_post',
@ -623,7 +641,7 @@ describe('emails sent for notifications', () => {
__typename: 'Comment', __typename: 'Comment',
id: 'comment-2', id: 'comment-2',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'mentioned_in_comment', reason: 'mentioned_in_comment',
@ -646,7 +664,7 @@ describe('emails sent for notifications', () => {
variables: { variables: {
id: 'post', id: 'post',
title: 'This is the post', title: 'This is the post',
content: `Hello, ${mentionString}, my trusty follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
groupId: 'public-group', groupId: 'public-group',
}, },
}) })
@ -666,7 +684,7 @@ describe('emails sent for notifications', () => {
mutation: createCommentMutation, mutation: createCommentMutation,
variables: { variables: {
id: 'comment-2', id: 'comment-2',
content: `Hello, ${mentionString}, my beloved follower.`, content: `Hello, ${mentionString}, my trusty followers.`,
postId: 'post', postId: 'post',
}, },
}) })
@ -694,7 +712,7 @@ describe('emails sent for notifications', () => {
__typename: 'Comment', __typename: 'Comment',
id: 'comment-2', id: 'comment-2',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'commented_on_post', reason: 'commented_on_post',
@ -706,7 +724,7 @@ describe('emails sent for notifications', () => {
__typename: 'Comment', __typename: 'Comment',
id: 'comment-2', id: 'comment-2',
content: content:
'Hello, <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>, my beloved follower.', 'Hello, <br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a><br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>, my trusty followers.',
}, },
read: false, read: false,
reason: 'mentioned_in_comment', reason: 'mentioned_in_comment',

View File

@ -14,14 +14,14 @@ import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock = jest.fn() const sendMailMock: (notification) => void = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({ jest.mock('@middleware/helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(), sendMail: (notification) => sendMailMock(notification),
})) }))
let server, query, mutate, authenticatedUser let server, query, mutate, authenticatedUser
let postAuthor, firstFollower, secondFollower let postAuthor, firstFollower, secondFollower, thirdFollower, emaillessFollower
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
@ -107,7 +107,7 @@ describe('following users notifications', () => {
slug: 'post-author', slug: 'post-author',
}, },
{ {
email: 'test@example.org', email: 'post-author@example.org',
password: '1234', password: '1234',
}, },
) )
@ -119,7 +119,7 @@ describe('following users notifications', () => {
slug: 'first-follower', slug: 'first-follower',
}, },
{ {
email: 'test2@example.org', email: 'first-follower@example.org',
password: '1234', password: '1234',
}, },
) )
@ -131,10 +131,27 @@ describe('following users notifications', () => {
slug: 'second-follower', slug: 'second-follower',
}, },
{ {
email: 'test3@example.org', email: 'second-follower@example.org',
password: '1234', password: '1234',
}, },
) )
thirdFollower = await Factory.build(
'user',
{
id: 'third-follower',
name: 'Third Follower',
slug: 'third-follower',
},
{
email: 'third-follower@example.org',
password: '1234',
},
)
emaillessFollower = await neode.create('User', {
id: 'email-less-follower',
name: 'Email-less Follower',
slug: 'email-less-follower',
})
await secondFollower.update({ emailNotificationsFollowingUsers: false }) await secondFollower.update({ emailNotificationsFollowingUsers: false })
authenticatedUser = await firstFollower.toJson() authenticatedUser = await firstFollower.toJson()
await mutate({ await mutate({
@ -146,6 +163,16 @@ describe('following users notifications', () => {
mutation: followUserMutation, mutation: followUserMutation,
variables: { id: 'post-author' }, variables: { id: 'post-author' },
}) })
authenticatedUser = await thirdFollower.toJson()
await mutate({
mutation: followUserMutation,
variables: { id: 'post-author' },
})
authenticatedUser = await emaillessFollower.toJson()
await mutate({
mutation: followUserMutation,
variables: { id: 'post-author' },
})
jest.clearAllMocks() jest.clearAllMocks()
}) })
@ -221,8 +248,43 @@ describe('following users notifications', () => {
}) })
}) })
it('sends only one email, as second follower has emails disabled', () => { it('sends notification to the email-less follower', async () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) authenticatedUser = await emaillessFollower.toJson()
await expect(
query({
query: notificationQuery,
}),
).resolves.toMatchObject({
data: {
notifications: [
{
from: {
__typename: 'Post',
id: 'post',
},
read: false,
reason: 'followed_user_posted',
},
],
},
errors: undefined,
})
})
it('sends only two emails, as second follower has emails disabled and email-less follower has no email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(2)
expect(sendMailMock).toHaveBeenCalledWith(
expect.objectContaining({
html: expect.stringContaining('Hello First Follower'),
to: 'first-follower@example.org',
}),
)
expect(sendMailMock).toHaveBeenCalledWith(
expect.objectContaining({
html: expect.stringContaining('Hello Third Follower'),
to: 'third-follower@example.org',
}),
)
}) })
}) })

View File

@ -17,22 +17,23 @@ import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock = jest.fn() const sendMailMock: (notification) => void = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({ jest.mock('@middleware/helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(), sendMail: (notification) => sendMailMock(notification),
})) }))
let server, query, mutate, authenticatedUser let server, query, mutate, authenticatedUser
let postAuthor, groupMember, pendingMember, noMember let postAuthor, groupMember, pendingMember, noMember, emaillessMember
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
const mentionString = ` 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="no-member" href="/profile/no-member/no-member">@no-member</a>
<a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member">@pending-member</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>. <a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member">@group-member</a>.
<a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member">@email-less-member</a>.
` `
const createPostMutation = gql` const createPostMutation = gql`
@ -168,6 +169,12 @@ describe('mentions in groups', () => {
password: '1234', password: '1234',
}, },
) )
emaillessMember = await neode.create('User', {
id: 'email-less-member',
name: 'Email-less Member',
slug: 'email-less-member',
})
authenticatedUser = await postAuthor.toJson() authenticatedUser = await postAuthor.toJson()
await mutate({ await mutate({
mutation: createGroupMutation(), mutation: createGroupMutation(),
@ -243,6 +250,28 @@ describe('mentions in groups', () => {
userId: 'pending-member', userId: 'pending-member',
}, },
}) })
authenticatedUser = await emaillessMember.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 postAuthor.toJson() authenticatedUser = await postAuthor.toJson()
await mutate({ await mutate({
mutation: changeGroupMemberRoleMutation(), mutation: changeGroupMemberRoleMutation(),
@ -260,8 +289,26 @@ describe('mentions in groups', () => {
roleInGroup: 'usual', roleInGroup: 'usual',
}, },
}) })
await mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'closed-group',
userId: 'email-less-member',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'hidden-group',
userId: 'email-less-member',
roleInGroup: 'usual',
},
})
authenticatedUser = await groupMember.toJson() authenticatedUser = await groupMember.toJson()
await markAllAsRead() await markAllAsRead()
authenticatedUser = await emaillessMember.toJson()
await markAllAsRead()
}) })
afterEach(async () => { afterEach(async () => {
@ -327,7 +374,7 @@ describe('mentions in groups', () => {
__typename: 'Post', __typename: 'Post',
id: 'public-post', id: 'public-post',
content: content:
'Hey <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-meber</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br>! Please read this', 'Hey <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-member</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>.<br>! Please read this',
}, },
read: false, read: false,
reason: 'post_in_group', reason: 'post_in_group',
@ -339,7 +386,7 @@ describe('mentions in groups', () => {
__typename: 'Post', __typename: 'Post',
id: 'public-post', id: 'public-post',
content: content:
'Hey <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-meber</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br>! Please read this', 'Hey <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-member</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>.<br>! Please read this',
}, },
read: false, read: false,
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
@ -351,7 +398,7 @@ describe('mentions in groups', () => {
}) })
}) })
it('sends 3 emails, one for each user', () => { it('sends only 3 emails, one for each user with an email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(3) expect(sendMailMock).toHaveBeenCalledTimes(3)
}) })
}) })
@ -423,7 +470,7 @@ describe('mentions in groups', () => {
__typename: 'Post', __typename: 'Post',
id: 'closed-post', id: 'closed-post',
content: content:
'Hey members <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-meber</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br>! Please read this', 'Hey members <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-member</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>.<br>! Please read this',
}, },
read: false, read: false,
reason: 'post_in_group', reason: 'post_in_group',
@ -435,7 +482,7 @@ describe('mentions in groups', () => {
__typename: 'Post', __typename: 'Post',
id: 'closed-post', id: 'closed-post',
content: content:
'Hey members <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-meber</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br>! Please read this', 'Hey members <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-member</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>.<br>! Please read this',
}, },
read: false, read: false,
reason: 'mentioned_in_post', reason: 'mentioned_in_post',
@ -519,7 +566,7 @@ describe('mentions in groups', () => {
__typename: 'Post', __typename: 'Post',
id: 'hidden-post', id: 'hidden-post',
content: content:
'Hey hiders <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-meber</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br>! Please read this', 'Hey hiders <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-member</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>.<br>! Please read this',
}, },
read: false, read: false,
reason: 'post_in_group', reason: 'post_in_group',
@ -531,7 +578,7 @@ describe('mentions in groups', () => {
__typename: 'Post', __typename: 'Post',
id: 'hidden-post', id: 'hidden-post',
content: content:
'Hey hiders <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-meber</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br>! Please read this', 'Hey hiders <br><a class="mention" data-mention-id="no-member" href="/profile/no-member/no-member" target="_blank">@no-member</a><br><a class="mention" data-mention-id="pending-member" href="/profile/pending-member/pending-member" target="_blank">@pending-member</a><br><a class="mention" data-mention-id="group-member" href="/profile/group-member/group-member" target="_blank">@group-member</a>.<br><a class="mention" data-mention-id="email-less-member" href="/profile/email-less-member/email-less-member" target="_blank">@email-less-member</a>.<br>! Please read this',
}, },
read: false, read: false,
reason: 'mentioned_in_post', reason: 'mentioned_in_post',

View File

@ -6,15 +6,20 @@ import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import CONFIG from '@config/index' import CONFIG from '@config/index'
import { cleanDatabase } from '@db/factories' import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j' import { getNeode, getDriver } from '@db/neo4j'
import createServer from '@src/server' import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({
sendMail: (notification) => sendMailMock(notification),
}))
let server, query, mutate, authenticatedUser let server, query, mutate, authenticatedUser
let postAuthor, firstCommenter, secondCommenter let postAuthor, firstCommenter, secondCommenter, emaillessObserver
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
@ -102,42 +107,47 @@ afterAll(async () => {
describe('notifications for users that observe a post', () => { describe('notifications for users that observe a post', () => {
beforeAll(async () => { beforeAll(async () => {
postAuthor = await neode.create( postAuthor = await Factory.build(
'User', 'user',
{ {
id: 'post-author', id: 'post-author',
name: 'Post Author', name: 'Post Author',
slug: 'post-author', slug: 'post-author',
}, },
{ {
email: 'test@example.org', email: 'post-author@example.org',
password: '1234', password: '1234',
}, },
) )
firstCommenter = await neode.create( firstCommenter = await Factory.build(
'User', 'user',
{ {
id: 'first-commenter', id: 'first-commenter',
name: 'First Commenter', name: 'First Commenter',
slug: 'first-commenter', slug: 'first-commenter',
}, },
{ {
email: 'test2@example.org', email: 'first-commenter@example.org',
password: '1234', password: '1234',
}, },
) )
secondCommenter = await neode.create( secondCommenter = await Factory.build(
'User', 'user',
{ {
id: 'second-commenter', id: 'second-commenter',
name: 'Second Commenter', name: 'Second Commenter',
slug: 'second-commenter', slug: 'second-commenter',
}, },
{ {
email: 'test3@example.org', email: 'second-commenter@example.org',
password: '1234', password: '1234',
}, },
) )
emaillessObserver = await neode.create('User', {
id: 'email-less-observer',
name: 'Email-less Observer',
slug: 'email-less-observer',
})
authenticatedUser = await postAuthor.toJson() authenticatedUser = await postAuthor.toJson()
await mutate({ await mutate({
mutation: createPostMutation, mutation: createPostMutation,
@ -147,6 +157,14 @@ describe('notifications for users that observe a post', () => {
content: 'This is the content of the post', content: 'This is the content of the post',
}, },
}) })
authenticatedUser = await emaillessObserver.toJson()
await mutate({
mutation: toggleObservePostMutation,
variables: {
id: 'post',
value: true,
},
})
}) })
describe('first comment on the post', () => { describe('first comment on the post', () => {
@ -198,8 +216,18 @@ describe('notifications for users that observe a post', () => {
}) })
}) })
it('sends one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(sendMailMock).toHaveBeenCalledWith(
expect.objectContaining({
to: 'post-author@example.org',
}),
)
})
describe('second comment on post', () => { describe('second comment on post', () => {
beforeAll(async () => { beforeAll(async () => {
jest.clearAllMocks()
authenticatedUser = await secondCommenter.toJson() authenticatedUser = await secondCommenter.toJson()
await mutate({ await mutate({
mutation: createCommentMutation, mutation: createCommentMutation,
@ -277,10 +305,25 @@ describe('notifications for users that observe a post', () => {
errors: undefined, errors: undefined,
}) })
}) })
it('sends two emails', () => {
expect(sendMailMock).toHaveBeenCalledTimes(2)
expect(sendMailMock).toHaveBeenCalledWith(
expect.objectContaining({
to: 'post-author@example.org',
}),
)
expect(sendMailMock).toHaveBeenCalledWith(
expect.objectContaining({
to: 'first-commenter@example.org',
}),
)
})
}) })
describe('first commenter unfollows the post and post author comments post', () => { describe('first commenter unfollows the post and post author comments post', () => {
beforeAll(async () => { beforeAll(async () => {
jest.clearAllMocks()
authenticatedUser = await firstCommenter.toJson() authenticatedUser = await firstCommenter.toJson()
await mutate({ await mutate({
mutation: toggleObservePostMutation, mutation: toggleObservePostMutation,
@ -376,6 +419,15 @@ describe('notifications for users that observe a post', () => {
errors: undefined, errors: undefined,
}) })
}) })
it('sends one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1)
expect(sendMailMock).toHaveBeenCalledWith(
expect.objectContaining({
to: 'second-commenter@example.org',
}),
)
})
}) })
}) })
}) })

View File

@ -13,9 +13,9 @@ import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock = jest.fn() const sendMailMock: (notification) => void = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({ jest.mock('@middleware/helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(), sendMail: (notification) => sendMailMock(notification),
})) }))
let isUserOnlineMock = jest.fn().mockReturnValue(false) let isUserOnlineMock = jest.fn().mockReturnValue(false)

View File

@ -17,14 +17,14 @@ import createServer from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock = jest.fn() const sendMailMock: (notification) => void = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({ jest.mock('@middleware/helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(), sendMail: (notification) => sendMailMock(notification),
})) }))
let server, query, mutate, authenticatedUser let server, query, mutate, authenticatedUser
let postAuthor, groupMember, pendingMember let postAuthor, groupMember, pendingMember, emaillessMember
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()
@ -159,6 +159,12 @@ describe('notify group members of new posts in group', () => {
password: '1234', password: '1234',
}, },
) )
emaillessMember = await neode.create('User', {
id: 'email-less-member',
name: 'Email-less Member',
slug: 'email-less-member',
})
authenticatedUser = await postAuthor.toJson() authenticatedUser = await postAuthor.toJson()
await mutate({ await mutate({
mutation: createGroupMutation(), mutation: createGroupMutation(),
@ -186,6 +192,14 @@ describe('notify group members of new posts in group', () => {
userId: 'pending-member', userId: 'pending-member',
}, },
}) })
authenticatedUser = await emaillessMember.toJson()
await mutate({
mutation: joinGroupMutation(),
variables: {
groupId: 'g-1',
userId: 'group-member',
},
})
authenticatedUser = await postAuthor.toJson() authenticatedUser = await postAuthor.toJson()
await mutate({ await mutate({
mutation: changeGroupMemberRoleMutation(), mutation: changeGroupMemberRoleMutation(),
@ -195,6 +209,14 @@ describe('notify group members of new posts in group', () => {
roleInGroup: 'usual', roleInGroup: 'usual',
}, },
}) })
await mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'g-1',
userId: 'email-less-member',
roleInGroup: 'usual',
},
})
}) })
afterEach(async () => { afterEach(async () => {

View File

@ -18,9 +18,9 @@ import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation'
import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation'
import createServer, { pubsub } from '@src/server' import createServer, { pubsub } from '@src/server'
const sendMailMock = jest.fn() const sendMailMock: (notification) => void = jest.fn()
jest.mock('../helpers/email/sendMail', () => ({ jest.mock('@middleware/helpers/email/sendMail', () => ({
sendMail: () => sendMailMock(), sendMail: (notification) => sendMailMock(notification),
})) }))
const chatMessageTemplateMock = jest.fn() const chatMessageTemplateMock = jest.fn()
@ -195,8 +195,8 @@ describe('notifications', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
commentContent = 'Commenters comment.' commentContent = 'Commenters comment.'
commentAuthor = await neode.create( commentAuthor = await Factory.build(
'User', 'user',
{ {
id: 'commentAuthor', id: 'commentAuthor',
name: 'Mrs Comment', name: 'Mrs Comment',
@ -345,8 +345,8 @@ describe('notifications', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
postAuthor = await neode.create( postAuthor = await Factory.build(
'User', 'user',
{ {
id: 'postAuthor', id: 'postAuthor',
name: 'Mrs Post', name: 'Mrs Post',
@ -658,8 +658,8 @@ describe('notifications', () => {
beforeEach(async () => { beforeEach(async () => {
commentContent = commentContent =
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.' 'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await neode.create( commentAuthor = await Factory.build(
'User', 'user',
{ {
id: 'commentAuthor', id: 'commentAuthor',
name: 'Mrs Comment', name: 'Mrs Comment',
@ -673,15 +673,15 @@ describe('notifications', () => {
}) })
it('sends only one notification with reason mentioned_in_comment', async () => { it('sends only one notification with reason mentioned_in_comment', async () => {
postAuthor = await neode.create( postAuthor = await Factory.build(
'User', 'user',
{ {
id: 'MrPostAuthor', id: 'MrPostAuthor',
name: 'Mr Author', name: 'Mr Author',
slug: 'mr-author', slug: 'mr-author',
}, },
{ {
email: 'post-author@example.org', email: 'post-author2@example.org',
password: '1234', password: '1234',
}, },
) )
@ -756,8 +756,8 @@ describe('notifications', () => {
await postAuthor.relateTo(notifiedUser, 'blocked') await postAuthor.relateTo(notifiedUser, 'blocked')
commentContent = commentContent =
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.' 'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await neode.create( commentAuthor = await Factory.build(
'User', 'user',
{ {
id: 'commentAuthor', id: 'commentAuthor',
name: 'Mrs Comment', name: 'Mrs Comment',
@ -807,8 +807,8 @@ describe('notifications', () => {
await postAuthor.relateTo(notifiedUser, 'muted') await postAuthor.relateTo(notifiedUser, 'muted')
commentContent = commentContent =
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.' 'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
commentAuthor = await neode.create( commentAuthor = await Factory.build(
'User', 'user',
{ {
id: 'commentAuthor', id: 'commentAuthor',
name: 'Mrs Comment', name: 'Mrs Comment',
@ -879,8 +879,8 @@ describe('notifications', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
chatSender = await neode.create( chatSender = await Factory.build(
'User', 'user',
{ {
id: 'chatSender', id: 'chatSender',
name: 'chatSender', name: 'chatSender',
@ -931,7 +931,7 @@ describe('notifications', () => {
content: 'Some nice message to chatReceiver', content: 'Some nice message to chatReceiver',
senderId: 'chatSender', senderId: 'chatSender',
username: 'chatSender', username: 'chatSender',
avatar: null, avatar: expect.any(String),
date: expect.any(String), date: expect.any(String),
saved: true, saved: true,
distributed: false, distributed: false,
@ -967,7 +967,7 @@ describe('notifications', () => {
content: 'Some nice message to chatReceiver', content: 'Some nice message to chatReceiver',
senderId: 'chatSender', senderId: 'chatSender',
username: 'chatSender', username: 'chatSender',
avatar: null, avatar: expect.any(String),
date: expect.any(String), date: expect.any(String),
saved: true, saved: true,
distributed: false, distributed: false,
@ -1046,7 +1046,7 @@ describe('notifications', () => {
content: 'Some nice message to chatReceiver', content: 'Some nice message to chatReceiver',
senderId: 'chatSender', senderId: 'chatSender',
username: 'chatSender', username: 'chatSender',
avatar: null, avatar: expect.any(String),
date: expect.any(String), date: expect.any(String),
saved: true, saved: true,
distributed: false, distributed: false,

View File

@ -18,59 +18,28 @@ import { pubsub, NOTIFICATION_ADDED, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } fr
import extractMentionedUsers from './mentions/extractMentionedUsers' import extractMentionedUsers from './mentions/extractMentionedUsers'
const queryNotificationEmails = async (context, notificationUserIds) => {
if (!notificationUserIds?.length) return []
const userEmailCypher = `
MATCH (user: User)
// blocked users are filtered out from notifications already
WHERE user.id in $notificationUserIds
WITH user
MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
RETURN emailAddress {.email}
`
const session = context.driver.session()
const writeTxResultPromise = session.readTransaction(async (transaction) => {
const emailAddressTransactionResponse = await transaction.run(userEmailCypher, {
notificationUserIds,
})
return emailAddressTransactionResponse.records.map((record) => record.get('emailAddress'))
})
try {
const emailAddresses = await writeTxResultPromise
return emailAddresses
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
}
const publishNotifications = async ( const publishNotifications = async (
context, context,
promises, notificationsPromise,
emailNotificationSetting: string, emailNotificationSetting: string,
emailsSent: string[] = [], emailsSent: string[] = [],
): Promise<string[]> => { ): Promise<string[]> => {
let notifications = await Promise.all(promises) const notifications = await notificationsPromise
notifications = notifications.flat() notifications.forEach((notificationAdded) => {
const notificationsEmailAddresses = await queryNotificationEmails(
context,
notifications.map((notification) => notification.to.id),
)
notifications.forEach((notificationAdded, index) => {
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
if ( if (
notificationAdded.email && // no primary email was found
(notificationAdded.to[emailNotificationSetting] ?? true) && (notificationAdded.to[emailNotificationSetting] ?? true) &&
!isUserOnline(notificationAdded.to) && !isUserOnline(notificationAdded.to) &&
!emailsSent.includes(notificationsEmailAddresses[index].email) !emailsSent.includes(notificationAdded.email)
) { ) {
sendMail( sendMail(
notificationTemplate({ notificationTemplate({
email: notificationsEmailAddresses[index].email, email: notificationAdded.email,
variables: { notification: notificationAdded }, variables: { notification: notificationAdded },
}), }),
) )
emailsSent.push(notificationsEmailAddresses[index].email) emailsSent.push(notificationAdded.email)
} }
}) })
return emailsSent return emailsSent
@ -82,7 +51,7 @@ const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => {
if (user) { if (user) {
await publishNotifications( await publishNotifications(
context, context,
[notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context)], notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context),
'emailNotificationsGroupMemberJoined', 'emailNotificationsGroupMemberJoined',
) )
} }
@ -95,7 +64,7 @@ const handleLeaveGroup = async (resolve, root, args, context, resolveInfo) => {
if (user) { if (user) {
await publishNotifications( await publishNotifications(
context, context,
[notifyOwnersOfGroup(groupId, userId, 'user_left_group', context)], notifyOwnersOfGroup(groupId, userId, 'user_left_group', context),
'emailNotificationsGroupMemberLeft', 'emailNotificationsGroupMemberLeft',
) )
} }
@ -108,7 +77,7 @@ const handleChangeGroupMemberRole = async (resolve, root, args, context, resolve
if (user) { if (user) {
await publishNotifications( await publishNotifications(
context, context,
[notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context)], notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context),
'emailNotificationsGroupMemberRoleChanged', 'emailNotificationsGroupMemberRoleChanged',
) )
} }
@ -121,7 +90,7 @@ const handleRemoveUserFromGroup = async (resolve, root, args, context, resolveIn
if (user) { if (user) {
await publishNotifications( await publishNotifications(
context, context,
[notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context)], notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context),
'emailNotificationsGroupMemberRemoved', 'emailNotificationsGroupMemberRemoved',
) )
} }
@ -135,20 +104,20 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo
if (post) { if (post) {
const sentEmails: string[] = await publishNotifications( const sentEmails: string[] = await publishNotifications(
context, context,
[notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)], notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context),
'emailNotificationsMention', 'emailNotificationsMention',
) )
sentEmails.concat( sentEmails.concat(
await publishNotifications( await publishNotifications(
context, context,
[notifyFollowingUsers(post.id, groupId, context)], notifyFollowingUsers(post.id, groupId, context),
'emailNotificationsFollowingUsers', 'emailNotificationsFollowingUsers',
sentEmails, sentEmails,
), ),
) )
await publishNotifications( await publishNotifications(
context, context,
[notifyGroupMembersOfNewPost(post.id, groupId, context)], notifyGroupMembersOfNewPost(post.id, groupId, context),
'emailNotificationsPostInGroup', 'emailNotificationsPostInGroup',
sentEmails, sentEmails,
) )
@ -164,20 +133,18 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id) idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id)
const sentEmails: string[] = await publishNotifications( const sentEmails: string[] = await publishNotifications(
context, context,
[ notifyUsersOfMention(
notifyUsersOfMention( 'Comment',
'Comment', comment.id,
comment.id, idsOfMentionedUsers,
idsOfMentionedUsers, 'mentioned_in_comment',
'mentioned_in_comment', context,
context, ),
),
],
'emailNotificationsMention', 'emailNotificationsMention',
) )
await publishNotifications( await publishNotifications(
context, context,
[notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context)], notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context),
'emailNotificationsCommentOnObservedPost', 'emailNotificationsCommentOnObservedPost',
sentEmails, sentEmails,
) )
@ -208,17 +175,20 @@ const notifyFollowingUsers = async (postId, groupId, context) => {
const cypher = ` const cypher = `
MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId })<-[:FOLLOWS]-(user:User) MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId })<-[:FOLLOWS]-(user:User)
OPTIONAL MATCH (post)-[:IN]->(group:Group { id: $groupId }) OPTIONAL MATCH (post)-[:IN]->(group:Group { id: $groupId })
WITH post, author, user, group WHERE group IS NULL OR group.groupType = 'public' OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
WITH post, author, user, emailAddress, group
WHERE group IS NULL OR group.groupType = 'public'
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
WITH notification, author, user, WITH notification, author, user, emailAddress.email as email,
post {.*, author: properties(author) } AS finalResource post {.*, author: properties(author) } AS finalResource
RETURN notification { RETURN notification {
.*, .*,
from: finalResource, from: finalResource,
to: properties(user), to: properties(user),
email: email,
relatedUser: properties(author) relatedUser: properties(author)
} }
` `
@ -233,8 +203,7 @@ const notifyFollowingUsers = async (postId, groupId, context) => {
return notificationTransactionResponse.records.map((record) => record.get('notification')) return notificationTransactionResponse.records.map((record) => record.get('notification'))
}) })
try { try {
const notifications = await writeTxResultPromise return await writeTxResultPromise
return notifications
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
@ -247,23 +216,25 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => {
const reason = 'post_in_group' const reason = 'post_in_group'
const cypher = ` const cypher = `
MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId }) MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId })
OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User) MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User)
WHERE NOT membership.role = 'pending' WHERE NOT membership.role = 'pending'
AND NOT (user)-[:MUTED]->(group) AND NOT (user)-[:MUTED]->(group)
AND NOT (user)-[:MUTED]->(author) AND NOT (user)-[:MUTED]->(author)
AND NOT (user)-[:BLOCKED]-(author) AND NOT (user)-[:BLOCKED]-(author)
AND NOT user.id = $userId AND NOT user.id = $userId
WITH post, author, user WITH post, author, user, emailAddress
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
SET notification.read = FALSE SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
WITH notification, author, user, WITH notification, author, user, emailAddress.email as email,
post {.*, author: properties(author) } AS finalResource post {.*, author: properties(author) } AS finalResource
RETURN notification { RETURN notification {
.*, .*,
from: finalResource, from: finalResource,
to: properties(user), to: properties(user),
email: email,
relatedUser: properties(author) relatedUser: properties(author)
} }
` `
@ -278,8 +249,7 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => {
return notificationTransactionResponse.records.map((record) => record.get('notification')) return notificationTransactionResponse.records.map((record) => record.get('notification'))
}) })
try { try {
const notifications = await writeTxResultPromise return await writeTxResultPromise
return notifications
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
@ -295,12 +265,13 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
WITH owner, group, user, membership WITH owner, group, user, membership
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner) MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner)
WITH group, owner, notification, user, membership WITH group, owner, notification, user, membership
OPTIONAL MATCH (owner)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
SET notification.read = FALSE SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
SET notification.relatedUserId = $userId SET notification.relatedUserId = $userId
WITH owner, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification WITH owner, emailAddress.email as email, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification
RETURN notification {.*, from: finalGroup, to: properties(owner), relatedUser: properties(user) } RETURN notification {.*, from: finalGroup, to: properties(owner), email: email, relatedUser: properties(user) }
` `
const session = context.driver.session() const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => { const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -312,8 +283,7 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
return notificationTransactionResponse.records.map((record) => record.get('notification')) return notificationTransactionResponse.records.map((record) => record.get('notification'))
}) })
try { try {
const notifications = await writeTxResultPromise return await writeTxResultPromise
return notifications
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
@ -327,17 +297,18 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => {
MATCH (owner:User { id: $ownerId }) MATCH (owner:User { id: $ownerId })
MATCH (user:User { id: $userId }) MATCH (user:User { id: $userId })
MATCH (group:Group { id: $groupId }) MATCH (group:Group { id: $groupId })
OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group) OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group)
WITH user, group, owner, membership WITH user, group, owner, membership, emailAddress
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user) MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH group, user, notification, owner, membership WITH group, user, notification, owner, membership, emailAddress
SET notification.read = FALSE SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
SET notification.relatedUserId = $ownerId SET notification.relatedUserId = $ownerId
WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup,
notification, user, owner notification, user, emailAddress.email as email, owner
RETURN notification {.*, from: finalGroup, to: properties(user), relatedUser: properties(owner) } RETURN notification {.*, from: finalGroup, to: properties(user), email: email, relatedUser: properties(owner) }
` `
const session = context.driver.session() const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => { const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -350,8 +321,7 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => {
return notificationTransactionResponse.records.map((record) => record.get('notification')) return notificationTransactionResponse.records.map((record) => record.get('notification'))
}) })
try { try {
const notifications = await writeTxResultPromise return await writeTxResultPromise
return notifications
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
@ -371,11 +341,13 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
WHERE user.id in $idsOfUsers WHERE user.id in $idsOfUsers
AND NOT (user)-[:BLOCKED]-(author) AND NOT (user)-[:BLOCKED]-(author)
AND NOT (user)-[:MUTED]->(author) AND NOT (user)-[:MUTED]->(author)
OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
OPTIONAL MATCH (post)-[:IN]->(group:Group) OPTIONAL MATCH (post)-[:IN]->(group:Group)
OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) 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'] WITH post, author, user, group, emailAddress
WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner']
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH post AS resource, notification, user WITH post AS resource, notification, user, emailAddress
` `
break break
} }
@ -388,25 +360,27 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
AND NOT (user)-[:BLOCKED]-(postAuthor) AND NOT (user)-[:BLOCKED]-(postAuthor)
AND NOT (user)-[:MUTED]->(commenter) AND NOT (user)-[:MUTED]->(commenter)
AND NOT (user)-[:MUTED]->(postAuthor) AND NOT (user)-[:MUTED]->(postAuthor)
OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
OPTIONAL MATCH (post)-[:IN]->(group:Group) OPTIONAL MATCH (post)-[:IN]->(group:Group)
OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) 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'] WITH comment, user, group, emailAddress
WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner']
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH comment AS resource, notification, user WITH comment AS resource, notification, user, emailAddress
` `
break break
} }
} }
mentionedCypher += ` mentionedCypher += `
WITH notification, user, resource, WITH notification, user, resource, emailAddress,
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors, [(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
WITH resource, user, notification, authors, posts, WITH resource, user, emailAddress.email as email, notification, authors, posts,
resource {.*, __typename: [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'Group']][0], author: authors[0], post: posts[0]} AS finalResource resource {.*, __typename: [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'Group']][0], author: authors[0], post: posts[0]} AS finalResource
SET notification.read = FALSE SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
RETURN notification {.*, from: finalResource, to: properties(user), relatedUser: properties(user) } RETURN notification {.*, from: finalResource, to: properties(user), email: email, relatedUser: properties(user) }
` `
const session = context.driver.session() const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => { const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -418,8 +392,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
return notificationTransactionResponse.records.map((record) => record.get('notification')) return notificationTransactionResponse.records.map((record) => record.get('notification'))
}) })
try { try {
const notifications = await writeTxResultPromise return await writeTxResultPromise
return notifications
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
@ -437,18 +410,20 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => {
WHERE NOT (observingUser)-[:BLOCKED]-(commenter) WHERE NOT (observingUser)-[:BLOCKED]-(commenter)
AND NOT (observingUser)-[:MUTED]->(commenter) AND NOT (observingUser)-[:MUTED]->(commenter)
AND NOT observingUser.id = $userId AND NOT observingUser.id = $userId
WITH observingUser, post, comment, commenter OPTIONAL MATCH (observingUser)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
WITH observingUser, emailAddress, post, comment, commenter
MATCH (postAuthor:User)-[:WROTE]->(post) MATCH (postAuthor:User)-[:WROTE]->(post)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser)
SET notification.read = FALSE SET notification.read = FALSE
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
SET notification.updatedAt = toString(datetime()) SET notification.updatedAt = toString(datetime())
WITH notification, observingUser, post, commenter, postAuthor, WITH notification, observingUser, emailAddress.email as email, post, commenter, postAuthor,
comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource
RETURN notification { RETURN notification {
.*, .*,
from: finalResource, from: finalResource,
to: properties(observingUser), to: properties(observingUser),
email: email,
relatedUser: properties(commenter) relatedUser: properties(commenter)
} }
`, `,
@ -461,8 +436,7 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => {
return notificationTransactionResponse.records.map((record) => record.get('notification')) return notificationTransactionResponse.records.map((record) => record.get('notification'))
}) })
try { try {
const notifications = await writeTxResultPromise return await writeTxResultPromise
return notifications
} finally { } finally {
session.close() session.close()
} }