/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return */ import gql from 'graphql-tag' import pubsubContext from '@context/pubsub' import Factory, { cleanDatabase } from '@db/factories' import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation' import { createGroupMutation } from '@graphql/queries/createGroupMutation' import { CreateMessage } from '@graphql/queries/CreateMessage' import { createRoomMutation } from '@graphql/queries/createRoomMutation' import { joinGroupMutation } from '@graphql/queries/joinGroupMutation' import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' import type { ApolloTestSetup } from '@root/test/helpers' import { createApolloTestSetup } from '@root/test/helpers' import type { Context } from '@src/context' import type { DecodedUser } from '@src/jwt/decode' const sendChatMessageMailMock: (notification) => void = jest.fn() const sendNotificationMailMock: (notification) => void = jest.fn() jest.mock('@src/emails/sendEmail', () => ({ sendChatMessageMail: (notification) => sendChatMessageMailMock(notification), sendNotificationMail: (notification) => sendNotificationMailMock(notification), })) let isUserOnlineMock = jest.fn() jest.mock('../helpers/isUserOnline', () => ({ isUserOnline: () => isUserOnlineMock(), })) const pubsub = pubsubContext() const pubsubSpy = jest.spyOn(pubsub, 'publish') let notifiedUser let authenticatedUser: Context['user'] const context = () => ({ authenticatedUser, pubsub }) let mutate: ApolloTestSetup['mutate'] let query: any // eslint-disable-line @typescript-eslint/no-explicit-any let database: ApolloTestSetup['database'] let server: ApolloTestSetup['server'] const categoryIds = ['cat9'] const createPostMutation = gql` mutation ($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) { CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) { id title content } } ` const updatePostMutation = gql` mutation ($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) { UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) { title content } } ` const createCommentMutation = gql` mutation ($id: ID, $postId: ID!, $commentContent: String!) { CreateComment(id: $id, postId: $postId, content: $commentContent) { id content } } ` beforeAll(async () => { await cleanDatabase() const apolloSetup = createApolloTestSetup({ context }) mutate = apolloSetup.mutate query = apolloSetup.query database = apolloSetup.database server = apolloSetup.server }) afterAll(async () => { await cleanDatabase() void server.stop() void database.driver.close() database.neode.close() }) beforeEach(async () => { notifiedUser = await Factory.build( 'user', { id: 'you', name: 'Al Capone', slug: 'al-capone', }, { email: 'test@example.org', password: '1234', }, ) await database.neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', icon: 'university', }) }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 afterEach(async () => { await cleanDatabase() }) describe('notifications', () => { 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 } } } } ` describe('authenticated', () => { beforeEach(async () => { authenticatedUser = await notifiedUser.toJson() }) describe('given another user', () => { let title let postContent let postAuthor const createPostAction = async () => { authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, variables: { id: 'p47', title, postContent, categoryIds, }, }) authenticatedUser = await notifiedUser.toJson() } let commentContent let commentAuthor const createCommentOnPostAction = async () => { await createPostAction() authenticatedUser = await commentAuthor.toJson() await mutate({ mutation: createCommentMutation, variables: { id: 'c47', postId: 'p47', commentContent, }, }) authenticatedUser = await notifiedUser.toJson() } describe('comments on my post', () => { beforeEach(async () => { title = 'My post' postContent = 'My post content.' postAuthor = notifiedUser }) describe('commenter is not me', () => { beforeEach(async () => { jest.clearAllMocks() commentContent = 'Commenters comment.' commentAuthor = await Factory.build( 'user', { id: 'commentAuthor', name: 'Mrs Comment', slug: 'mrs-comment', }, { email: 'commentauthor@example.org', password: '1234', }, ) }) it('sends me a notification and email', async () => { await createCommentOnPostAction() await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toMatchObject( expect.objectContaining({ data: { notifications: [ { read: false, createdAt: expect.any(String), reason: 'commented_on_post', from: { __typename: 'Comment', id: 'c47', content: commentContent, }, relatedUser: null, }, ], }, }), ) // Mail expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith( expect.objectContaining({ reason: 'commented_on_post', email: 'test@example.org', }), ) }) describe('if I have disabled `emailNotificationsCommentOnObservedPost`', () => { it('sends me a notification but no email', async () => { await notifiedUser.update({ emailNotificationsCommentOnObservedPost: false }) await createCommentOnPostAction() await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toMatchObject( expect.objectContaining({ data: { notifications: [ { read: false, createdAt: expect.any(String), reason: 'commented_on_post', from: { __typename: 'Comment', id: 'c47', content: commentContent, }, relatedUser: null, }, ], }, }), ) // No Mail expect(sendNotificationMailMock).not.toHaveBeenCalled() }) }) describe('if I have blocked the comment author', () => { it('sends me no notification', async () => { await notifiedUser.relateTo(commentAuthor, 'blocked') await createCommentOnPostAction() const expected = expect.objectContaining({ data: { notifications: [] }, }) await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toEqual(expected) }) }) describe('if I have muted the comment author', () => { it('sends me no notification', async () => { await notifiedUser.relateTo(commentAuthor, 'muted') await createCommentOnPostAction() const expected = expect.objectContaining({ data: { notifications: [] }, }) await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toEqual(expected) }) }) }) describe('commenter is me', () => { beforeEach(async () => { commentContent = 'My comment.' commentAuthor = notifiedUser }) it('sends me no notification', async () => { await createCommentOnPostAction() const expected = expect.objectContaining({ data: { notifications: [] }, }) await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toEqual(expected) }) }) }) beforeEach(async () => { jest.clearAllMocks() postAuthor = await Factory.build( 'user', { id: 'postAuthor', name: 'Mrs Post', slug: 'mrs-post', }, { email: 'post-author@example.org', password: '1234', }, ) }) describe('mentions me in a post', () => { beforeEach(async () => { title = 'Mentioning Al Capone' postContent = 'Hey @al-capone how do you do?' }) it('sends me a notification and email', async () => { await createPostAction() const expectedContent = 'Hey @al-capone how do you do?' await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toMatchObject({ errors: undefined, data: { notifications: [ { read: false, createdAt: expect.any(String), reason: 'mentioned_in_post', from: { __typename: 'Post', id: 'p47', content: expectedContent, }, }, ], }, }) // Mail expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith( expect.objectContaining({ reason: 'mentioned_in_post', email: 'test@example.org', }), ) }) describe('if I have disabled `emailNotificationsMention`', () => { it('sends me a notification but no email', async () => { await notifiedUser.update({ emailNotificationsMention: false }) await createPostAction() const expectedContent = 'Hey @al-capone how do you do?' await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toMatchObject({ errors: undefined, data: { notifications: [ { read: false, createdAt: expect.any(String), reason: 'mentioned_in_post', from: { __typename: 'Post', id: 'p47', content: expectedContent, }, }, ], }, }) // Mail expect(sendNotificationMailMock).not.toHaveBeenCalled() }) }) it('publishes `NOTIFICATION_ADDED` to me', async () => { await createPostAction() expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ reason: 'mentioned_in_post', to: expect.objectContaining({ id: 'you', }), }), }), ) expect(pubsubSpy).toHaveBeenCalledTimes(1) }) describe('updates the post and mentions me again', () => { const updatePostAction = async () => { const updatedContent = ` One more mention to @al-capone and again: @al-capone and again @al-capone ` authenticatedUser = await postAuthor.toJson() await mutate({ mutation: updatePostMutation, variables: { id: 'p47', title, postContent: updatedContent, categoryIds, }, }) authenticatedUser = await notifiedUser.toJson() } it('creates no duplicate notification for the same resource', async () => { const expectedUpdatedContent = '
One more mention to

@al-capone

and again:

@al-capone

and again

@al-capone

' await createPostAction() await updatePostAction() const expected = expect.objectContaining({ data: { notifications: [ { read: false, createdAt: expect.any(String), reason: 'mentioned_in_post', from: { __typename: 'Post', id: 'p47', content: expectedUpdatedContent, }, relatedUser: null, }, ], }, }) await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toEqual(expected) }) describe('if the notification was marked as read earlier', () => { const markAsReadAction = async () => { const mutation = gql` mutation ($id: ID!) { markAsRead(id: $id) { read } } ` await mutate({ mutation, variables: { id: 'p47' } }) } describe('but the next mention happens after the notification was marked as read', () => { it('sets the `read` attribute to false again', async () => { await createPostAction() await markAsReadAction() const { data: { notifications: [{ read: readBefore }], }, } = await query({ query: notificationQuery, }) await updatePostAction() const { data: { notifications: [{ read: readAfter }], }, } = await query({ query: notificationQuery, }) expect(readBefore).toEqual(true) expect(readAfter).toEqual(false) }) it('does not update the `createdAt` attribute', async () => { await createPostAction() await markAsReadAction() const { data: { notifications: [{ createdAt: createdAtBefore }], }, } = await query({ query: notificationQuery, }) await updatePostAction() const { data: { notifications: [{ createdAt: createdAtAfter }], }, } = await query({ query: notificationQuery, }) expect(createdAtBefore).toBeTruthy() expect(Date.parse(createdAtBefore)).toEqual(expect.any(Number)) expect(createdAtAfter).toBeTruthy() expect(Date.parse(createdAtAfter)).toEqual(expect.any(Number)) expect(createdAtBefore).toEqual(createdAtAfter) }) }) }) }) describe('but the author of the post blocked me', () => { beforeEach(async () => { await postAuthor.relateTo(notifiedUser, 'blocked') }) it('sends no notification', async () => { await createPostAction() const expected = expect.objectContaining({ data: { notifications: [] }, }) await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toEqual(expected) }) it('does not publish `NOTIFICATION_ADDED`', async () => { await createPostAction() expect(pubsubSpy).not.toHaveBeenCalled() }) }) describe('but the author of the post muted me', () => { beforeEach(async () => { await postAuthor.relateTo(notifiedUser, 'muted') }) it('sends me a notification', async () => { await createPostAction() const expected = expect.objectContaining({ data: { notifications: [ { createdAt: expect.any(String), from: { __typename: 'Post', content: 'Hey @al-capone how do you do?', id: 'p47', }, read: false, reason: 'mentioned_in_post', relatedUser: null, }, ], }, }) await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toEqual(expected) }) it('publishes `NOTIFICATION_ADDED`', async () => { await createPostAction() expect(pubsubSpy).toHaveBeenCalled() }) }) }) describe('mentions me in a comment', () => { beforeEach(async () => { title = 'Post where I get mentioned in a comment' postContent = 'Content of post where I get mentioned in a comment.' }) describe('I am not blocked at all', () => { beforeEach(async () => { commentContent = 'One mention about me with @al-capone.' commentAuthor = await Factory.build( 'user', { id: 'commentAuthor', name: 'Mrs Comment', slug: 'mrs-comment', }, { email: 'comment-author@example.org', password: '1234', }, ) }) it('sends only one notification with reason mentioned_in_comment', async () => { postAuthor = await Factory.build( 'user', { id: 'MrPostAuthor', name: 'Mr Author', slug: 'mr-author', }, { email: 'post-author2@example.org', password: '1234', }, ) await createCommentOnPostAction() const expected = expect.objectContaining({ data: { notifications: [ { read: false, createdAt: expect.any(String), reason: 'mentioned_in_comment', from: { __typename: 'Comment', id: 'c47', content: commentContent, }, relatedUser: null, }, ], }, }) await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toEqual(expected) }) beforeEach(async () => { title = "Post where I'm the author and I get mentioned in a comment" postContent = 'Content of post where I get mentioned in a comment.' postAuthor = notifiedUser }) it('sends only one notification with reason commented_on_post, no notification with reason mentioned_in_comment', async () => { await createCommentOnPostAction() const expected = { data: { notifications: [ { read: false, createdAt: expect.any(String), reason: 'commented_on_post', from: { __typename: 'Comment', id: 'c47', content: commentContent, }, relatedUser: null, }, ], }, } await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toMatchObject({ ...expected, errors: undefined }) }) }) describe('but the author of the post blocked me', () => { beforeEach(async () => { await postAuthor.relateTo(notifiedUser, 'blocked') commentContent = 'One mention about me with @al-capone.' commentAuthor = await Factory.build( 'user', { id: 'commentAuthor', name: 'Mrs Comment', slug: 'mrs-comment', }, { email: 'comment-author@example.org', password: '1234', }, ) }) it('sends no notification', async () => { await createCommentOnPostAction() await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toMatchObject({ data: { notifications: [] }, errors: undefined, }) }) it('does not publish `NOTIFICATION_ADDED` to authenticated user', async () => { await createCommentOnPostAction() expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ reason: 'commented_on_post', to: expect.objectContaining({ id: 'postAuthor', // that's expected, it's not me but the post author }), }), }), ) expect(pubsubSpy).toHaveBeenCalledTimes(1) }) }) describe('but the author of the post muted me', () => { beforeEach(async () => { await postAuthor.relateTo(notifiedUser, 'muted') commentContent = 'One mention about me with @al-capone.' commentAuthor = await Factory.build( 'user', { id: 'commentAuthor', name: 'Mrs Comment', slug: 'mrs-comment', }, { email: 'comment-author@example.org', password: '1234', }, ) }) it('sends me a notification', async () => { await createCommentOnPostAction() await expect( query({ query: notificationQuery, variables: { read: false, }, }), ).resolves.toMatchObject({ data: { notifications: [ { createdAt: expect.any(String), from: { __typename: 'Comment', content: 'One mention about me with @al-capone.', id: 'c47', }, read: false, reason: 'mentioned_in_comment', relatedUser: null, }, ], }, errors: undefined, }) }) it('publishes `NOTIFICATION_ADDED` to authenticated user and me', async () => { await createCommentOnPostAction() expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ reason: 'commented_on_post', to: expect.objectContaining({ id: 'postAuthor', // that's expected, it's not me but the post author }), }), }), ) expect(pubsubSpy).toHaveBeenCalledTimes(2) }) }) }) }) }) describe('chat notifications', () => { let chatSender let chatReceiver let roomId beforeEach(async () => { jest.clearAllMocks() chatSender = await Factory.build( 'user', { id: 'chatSender', name: 'chatSender', slug: 'chatSender', }, { email: 'chatSender@example.org', password: '1234', }, ) chatReceiver = await Factory.build( 'user', { id: 'chatReceiver', name: 'chatReceiver', slug: 'chatReceiver' }, { email: 'user@example.org' }, ) authenticatedUser = await chatSender.toJson() const room = await mutate({ mutation: createRoomMutation(), variables: { userId: 'chatReceiver', }, }) roomId = (room.data as any).CreateRoom.id // eslint-disable-line @typescript-eslint/no-explicit-any }) describe('if the chatReceiver is online', () => { it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(true) await mutate({ mutation: CreateMessage, variables: { roomId, content: 'Some nice message to chatReceiver', }, }) expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { roomCountUpdated: '1', userId: 'chatReceiver', }) expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { chatMessageAdded: expect.objectContaining({ id: expect.any(String), content: 'Some nice message to chatReceiver', senderId: 'chatSender', username: 'chatSender', avatar: expect.any(String), date: expect.any(String), saved: true, distributed: false, seen: false, }), userId: 'chatReceiver', }) expect(sendChatMessageMailMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver is offline', () => { it('publishes subscriptions and sends an email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await mutate({ mutation: CreateMessage, variables: { roomId, content: 'Some nice message to chatReceiver', }, }) expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { roomCountUpdated: '1', userId: 'chatReceiver', }) expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { chatMessageAdded: expect.objectContaining({ id: expect.any(String), content: 'Some nice message to chatReceiver', senderId: 'chatSender', username: 'chatSender', avatar: expect.any(String), date: expect.any(String), saved: true, distributed: false, seen: false, }), userId: 'chatReceiver', }) expect(sendChatMessageMailMock).toHaveBeenCalledTimes(1) expect(sendChatMessageMailMock).toHaveBeenCalledWith({ email: 'user@example.org', senderUser: expect.objectContaining({ name: 'chatSender', slug: 'chatsender', id: 'chatSender', }), recipientUser: expect.objectContaining({ name: 'chatReceiver', slug: 'chatreceiver', id: 'chatReceiver', }), }) }) }) describe('if the chatReceiver has blocked chatSender', () => { it('publishes no subscriptions and sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.relateTo(chatSender, 'blocked') await mutate({ mutation: CreateMessage, variables: { roomId, content: 'Some nice message to chatReceiver', }, }) expect(pubsubSpy).not.toHaveBeenCalled() expect(pubsubSpy).not.toHaveBeenCalled() expect(sendChatMessageMailMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver has muted chatSender', () => { it('publishes no subscriptions and sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.relateTo(chatSender, 'muted') await mutate({ mutation: CreateMessage, variables: { roomId, content: 'Some nice message to chatReceiver', }, }) expect(pubsubSpy).not.toHaveBeenCalled() expect(pubsubSpy).not.toHaveBeenCalled() expect(sendChatMessageMailMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => { it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.update({ emailNotificationsChatMessage: false }) await mutate({ mutation: CreateMessage, variables: { roomId, content: 'Some nice message to chatReceiver', }, }) expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { roomCountUpdated: '1', userId: 'chatReceiver', }) expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { chatMessageAdded: expect.objectContaining({ id: expect.any(String), content: 'Some nice message to chatReceiver', senderId: 'chatSender', username: 'chatSender', avatar: expect.any(String), date: expect.any(String), saved: true, distributed: false, seen: false, }), userId: 'chatReceiver', }) expect(sendChatMessageMailMock).not.toHaveBeenCalled() }) }) }) describe('group notifications', () => { let groupOwner beforeEach(async () => { groupOwner = await Factory.build( 'user', { id: 'group-owner', name: 'Group Owner', slug: 'group-owner', }, { email: 'owner@example.org', password: '1234', }, ) authenticatedUser = await groupOwner.toJson() await mutate({ mutation: createGroupMutation(), variables: { id: 'closed-group', name: 'The Closed Group', about: 'Will test the closed group!', description: 'Some description' + Array(50).join('_'), groupType: 'public', actionRadius: 'regional', categoryIds, }, }) }) describe('user joins group', () => { const joinGroupAction = async () => { authenticatedUser = (await notifiedUser.toJson()) as DecodedUser await mutate({ mutation: joinGroupMutation(), variables: { groupId: 'closed-group', userId: authenticatedUser.id, }, }) authenticatedUser = await groupOwner.toJson() } beforeEach(async () => { jest.clearAllMocks() }) it('sends the group owner a notification and email', async () => { await joinGroupAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: [ { read: false, reason: 'user_joined_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'you', }, }, ], }, errors: undefined, }) // Mail expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith( expect.objectContaining({ reason: 'user_joined_group', email: 'owner@example.org', }), ) }) describe('if the group owner has disabled `emailNotificationsGroupMemberJoined`', () => { it('sends the group owner a notification but no email', async () => { await groupOwner.update({ emailNotificationsGroupMemberJoined: false }) await joinGroupAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: [ { read: false, reason: 'user_joined_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'you', }, }, ], }, errors: undefined, }) // Mail expect(sendNotificationMailMock).not.toHaveBeenCalled() }) }) }) describe('user joins and leaves group', () => { const leaveGroupAction = async () => { authenticatedUser = (await notifiedUser.toJson()) as DecodedUser await mutate({ mutation: leaveGroupMutation(), variables: { groupId: 'closed-group', userId: authenticatedUser.id, }, }) authenticatedUser = await groupOwner.toJson() } beforeEach(async () => { jest.clearAllMocks() authenticatedUser = (await notifiedUser.toJson()) as DecodedUser await mutate({ mutation: joinGroupMutation(), variables: { groupId: 'closed-group', userId: authenticatedUser.id, }, }) }) it('sends the group owner two notifications and emails', async () => { await leaveGroupAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: expect.arrayContaining([ { read: false, reason: 'user_left_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'you', }, }, { read: false, reason: 'user_joined_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'you', }, }, ]), }, errors: undefined, }) // Mail expect(sendNotificationMailMock).toHaveBeenCalledTimes(2) expect(sendNotificationMailMock).toHaveBeenCalledWith( expect.objectContaining({ reason: 'user_joined_group', email: 'owner@example.org', }), ) expect(sendNotificationMailMock).toHaveBeenCalledWith( expect.objectContaining({ reason: 'user_left_group', email: 'owner@example.org', }), ) }) describe('if the group owner has disabled `emailNotificationsGroupMemberLeft`', () => { it('sends the group owner two notification but only only one email', async () => { await groupOwner.update({ emailNotificationsGroupMemberLeft: false }) await leaveGroupAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: expect.arrayContaining([ { read: false, reason: 'user_left_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'you', }, }, { read: false, reason: 'user_joined_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'you', }, }, ]), }, errors: undefined, }) // Mail expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) }) }) }) describe('user role in group changes', () => { const changeGroupMemberRoleAction = async () => { authenticatedUser = (await groupOwner.toJson()) as DecodedUser await mutate({ mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'closed-group', userId: 'you', roleInGroup: 'admin', }, }) authenticatedUser = await notifiedUser.toJson() } beforeEach(async () => { authenticatedUser = (await notifiedUser.toJson()) as DecodedUser await mutate({ mutation: joinGroupMutation(), variables: { groupId: 'closed-group', userId: authenticatedUser.id, }, }) // Clear after because the above generates a notification not related jest.clearAllMocks() }) it('sends the group member a notification and email', async () => { await changeGroupMemberRoleAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: [ { read: false, reason: 'changed_group_member_role', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'group-owner', }, }, ], }, errors: undefined, }) // Mail expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith( expect.objectContaining({ reason: 'changed_group_member_role', email: 'test@example.org', }), ) }) describe('if the group member has disabled `emailNotificationsGroupMemberRoleChanged`', () => { it('sends the group member a notification but no email', async () => { notifiedUser.update({ emailNotificationsGroupMemberRoleChanged: false }) await changeGroupMemberRoleAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: [ { read: false, reason: 'changed_group_member_role', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'group-owner', }, }, ], }, errors: undefined, }) // Mail expect(sendNotificationMailMock).not.toHaveBeenCalled() }) }) }) describe('user is removed from group', () => { const removeUserFromGroupAction = async () => { authenticatedUser = await groupOwner.toJson() await mutate({ mutation: removeUserFromGroupMutation(), variables: { groupId: 'closed-group', userId: 'you', }, }) authenticatedUser = await notifiedUser.toJson() } beforeEach(async () => { authenticatedUser = (await notifiedUser.toJson()) as DecodedUser await mutate({ mutation: joinGroupMutation(), variables: { groupId: 'closed-group', userId: authenticatedUser.id, }, }) // Clear after because the above generates a notification not related jest.clearAllMocks() }) it('sends the previous group member a notification and email', async () => { await removeUserFromGroupAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: [ { read: false, reason: 'removed_user_from_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'group-owner', }, }, ], }, errors: undefined, }) // Mail expect(sendNotificationMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith( expect.objectContaining({ reason: 'removed_user_from_group', email: 'test@example.org', }), ) }) describe('if the previous group member has disabled `emailNotificationsGroupMemberRemoved`', () => { it('sends the previous group member a notification but no email', async () => { notifiedUser.update({ emailNotificationsGroupMemberRemoved: false }) await removeUserFromGroupAction() await expect( query({ query: notificationQuery, }), ).resolves.toMatchObject({ data: { notifications: [ { read: false, reason: 'removed_user_from_group', createdAt: expect.any(String), from: { __typename: 'Group', id: 'closed-group', }, relatedUser: { id: 'group-owner', }, }, ], }, errors: undefined, }) // Mail expect(sendNotificationMailMock).not.toHaveBeenCalled() }) }) }) }) })