diff --git a/backend/package.json b/backend/package.json index 1822a52ca..ba09981b0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -70,7 +70,7 @@ "lodash": "~4.17.14", "merge-graphql-schemas": "^1.7.6", "metascraper": "^5.11.0", - "metascraper-audio": "^5.10.7", + "metascraper-audio": "^5.11.1", "metascraper-author": "^5.10.7", "metascraper-clearbit-logo": "^5.3.0", "metascraper-date": "^5.10.7", @@ -112,7 +112,7 @@ "@babel/plugin-proposal-throw-expressions": "^7.8.3", "@babel/preset-env": "~7.8.4", "@babel/register": "^7.8.3", - "apollo-server-testing": "~2.10.0", + "apollo-server-testing": "~2.10.1", "babel-core": "~7.0.0-0", "babel-eslint": "~10.0.3", "babel-jest": "~25.1.0", diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index 4636b8e9f..64eca97c8 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -2,11 +2,21 @@ import extractMentionedUsers from './mentions/extractMentionedUsers' import { validateNotifyUsers } from '../validation/validationMiddleware' import { pubsub, NOTIFICATION_ADDED } from '../../server' +const publishNotifications = async (...promises) => { + const notifications = await Promise.all(promises) + notifications + .flat() + .forEach(notificationAdded => pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })) +} + const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { const idsOfUsers = extractMentionedUsers(args.content) const post = await resolve(root, args, context, resolveInfo) - if (post && idsOfUsers && idsOfUsers.length) - await notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context) + if (post) { + await publishNotifications( + notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context), + ) + } return post } @@ -16,10 +26,10 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI const comment = await resolve(root, args, context, resolveInfo) const [postAuthor] = await postAuthorOfComment(comment.id, { context }) idsOfUsers = idsOfUsers.filter(id => id !== postAuthor.id) - if (idsOfUsers && idsOfUsers.length) - await notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context) - if (context.user.id !== postAuthor.id) - await notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context) + await publishNotifications( + notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context), + notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context), + ) return comment } @@ -29,7 +39,7 @@ const postAuthorOfComment = async (commentId, { context }) => { try { postAuthorId = await session.readTransaction(transaction => { return transaction.run( - ` + ` MATCH (author:User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId }) RETURN author { .id } as authorId `, @@ -43,6 +53,7 @@ const postAuthorOfComment = async (commentId, { context }) => { } const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { + if (!(idsOfUsers && idsOfUsers.length)) return [] await validateNotifyUsers(label, reason) let mentionedCypher switch (reason) { @@ -91,8 +102,8 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { return notificationTransactionResponse.records.map(record => record.get('notification')) }) try { - const [notification] = await writeTxResultPromise - return pubsub.publish(NOTIFICATION_ADDED, { notificationAdded: notification }) + const notifications = await writeTxResultPromise + return notifications } catch (error) { throw new Error(error) } finally { @@ -101,6 +112,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { } const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, context) => { + if (context.user.id === postAuthorId) return [] await validateNotifyUsers(label, reason) const session = context.driver.session() const writeTxResultPromise = await session.writeTransaction(async transaction => { @@ -121,8 +133,8 @@ const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, cont return notificationTransactionResponse.records.map(record => record.get('notification')) }) try { - const [notification] = await writeTxResultPromise - return pubsub.publish(NOTIFICATION_ADDED, { notificationAdded: notification }) + const notifications = await writeTxResultPromise + return notifications } finally { session.close() } diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js index 95c0037b8..af4ed9693 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -2,9 +2,10 @@ import { gql } from '../../helpers/jest' import { cleanDatabase } from '../../db/factories' import { createTestClient } from 'apollo-server-testing' import { getNeode, getDriver } from '../../db/neo4j' -import createServer from '../../server' +import createServer, { pubsub } from '../../server' let server, query, mutate, notifiedUser, authenticatedUser +let publishSpy const driver = getDriver() const neode = getNeode() const categoryIds = ['cat9'] @@ -36,6 +37,7 @@ const createCommentMutation = gql` beforeAll(async () => { await cleanDatabase() + publishSpy = jest.spyOn(pubsub, 'publish') const createServerResult = createServer({ context: () => { return { @@ -52,6 +54,7 @@ beforeAll(async () => { }) beforeEach(async () => { + publishSpy.mockClear() notifiedUser = await neode.create( 'User', { @@ -259,7 +262,15 @@ describe('notifications', () => { await createPostAction() const expectedContent = 'Hey @al-capone how do you do?' - const expected = expect.objectContaining({ + await expect( + query({ + query: notificationQuery, + variables: { + read: false, + }, + }), + ).resolves.toMatchObject({ + errors: undefined, data: { notifications: [ { @@ -275,15 +286,22 @@ describe('notifications', () => { ], }, }) + }) - await expect( - query({ - query: notificationQuery, - variables: { - read: false, - }, + it('publishes `NOTIFICATION_ADDED` to me', async () => { + await createPostAction() + expect(publishSpy).toHaveBeenCalledWith( + 'NOTIFICATION_ADDED', + expect.objectContaining({ + notificationAdded: expect.objectContaining({ + reason: 'mentioned_in_post', + to: expect.objectContaining({ + id: 'you', + }), + }), }), - ).resolves.toEqual(expected) + ) + expect(publishSpy).toHaveBeenCalledTimes(1) }) describe('updates the post and mentions me again', () => { @@ -429,6 +447,11 @@ describe('notifications', () => { }), ).resolves.toEqual(expected) }) + + it('does not publish `NOTIFICATION_ADDED`', async () => { + await createPostAction() + expect(publishSpy).not.toHaveBeenCalled() + }) }) }) @@ -505,7 +528,7 @@ describe('notifications', () => { }) it('sends only one notification with reason commented_on_post, no notification with reason mentioned_in_comment', async () => { await createCommentOnPostAction() - const expected = expect.objectContaining({ + const expected = { data: { notifications: [ { @@ -520,7 +543,7 @@ describe('notifications', () => { }, ], }, - }) + } await expect( query({ @@ -529,7 +552,7 @@ describe('notifications', () => { read: false, }, }), - ).resolves.toEqual(expected) + ).resolves.toMatchObject(expected, { errors: undefined }) }) }) @@ -554,10 +577,6 @@ describe('notifications', () => { it('sends no notification', async () => { await createCommentOnPostAction() - const expected = expect.objectContaining({ - data: { notifications: [] }, - }) - await expect( query({ query: notificationQuery, @@ -565,7 +584,26 @@ describe('notifications', () => { read: false, }, }), - ).resolves.toEqual(expected) + ).resolves.toMatchObject({ + data: { notifications: [] }, + errors: undefined, + }) + }) + + it('does not publish `NOTIFICATION_ADDED` to authenticated user', async () => { + await createCommentOnPostAction() + expect(publishSpy).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(publishSpy).toHaveBeenCalledTimes(1) }) }) }) diff --git a/backend/yarn.lock b/backend/yarn.lock index 8bc2e7c00..b4d497ac5 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1860,10 +1860,10 @@ apollo-engine-reporting-protobuf@^0.4.4: dependencies: "@apollo/protobufjs" "^1.0.3" -apollo-engine-reporting@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.5.0.tgz#6e3746de14fc87e14c289c0776a2d350e6f50918" - integrity sha512-Pe2DelijZ2QHqkqv8E97iOb32l+FIMT2nxpQsuH+nWi+96cCFJJJHjm3RLAPEUuvGOgW9dFYQP3J91EyC5O0tQ== +apollo-engine-reporting@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.6.0.tgz#a5073a2e350ea4c8ce6adb5a5b536028ed165390" + integrity sha512-prA17Tp/WYBJdCd4ey1CnGX8d4Xis1n9PsFmT7x8PV/oNpxG21/x3yNw5kPBZuKAoKz8yEggYtHhkYie1ZBjPQ== dependencies: apollo-engine-reporting-protobuf "^0.4.4" apollo-graphql "^0.4.0" @@ -1943,10 +1943,10 @@ apollo-server-caching@^0.5.1: dependencies: lru-cache "^5.0.0" -apollo-server-core@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.10.0.tgz#b8d51bdffe6529f0e3ca670ee8f1238765cfade4" - integrity sha512-x/UK6XvU307W8D/pzTclU04JIjRarcbg5mFPe0nVmO4OTc26uQgKi1WlZkcewXsAUnn+nDwKVn2c2G3dHEgXzQ== +apollo-server-core@^2.10.0, apollo-server-core@^2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.10.1.tgz#5fa4ce7992d0bf1cce616dedf1a22a41c7589c7c" + integrity sha512-BVITSJRMnj+CWFkjt7FMcaoqg/Ni9gfyVE9iu8bUc1IebBfFDcQj652Iolr7dTqyUziN2jbf0wfcybKYJLQHQQ== dependencies: "@apollographql/apollo-tools" "^0.4.3" "@apollographql/graphql-playground-html" "1.6.24" @@ -1954,7 +1954,7 @@ apollo-server-core@^2.10.0: "@types/ws" "^6.0.0" apollo-cache-control "^0.8.11" apollo-datasource "^0.7.0" - apollo-engine-reporting "^1.5.0" + apollo-engine-reporting "^1.6.0" apollo-server-caching "^0.5.1" apollo-server-env "^2.4.3" apollo-server-errors "^2.3.4" @@ -2012,12 +2012,12 @@ apollo-server-plugin-base@^0.6.10: dependencies: apollo-server-types "^0.2.10" -apollo-server-testing@~2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.10.0.tgz#c8d7fc2d4e6eaf84232aaa7c125d9fae691fbcf4" - integrity sha512-wBJ/CT3ZN5nmSySMqgpAFwX/I3yzsQhRGR8MCK/16MjhEZH6svNaJWzoif6gaocj0NyVBJvOIijuMTecG9+6vg== +apollo-server-testing@~2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.10.1.tgz#c493c41f51c122b3d87c0e5ffba4f0590b924593" + integrity sha512-KsvLzDb/mIf5h93QUxGXymywZq8urnXUPqckBxyNaF08puAO8VO0c4EE0VvuVZnelKZvlKlU0tYQQNQsc9iHfg== dependencies: - apollo-server-core "^2.10.0" + apollo-server-core "^2.10.1" apollo-server-types@^0.2.10: version "0.2.10" @@ -6197,12 +6197,12 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== -metascraper-audio@^5.10.7: - version "5.10.7" - resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.10.7.tgz#ba9f8333a7b71d388a0bf88dff64fc4f06595566" - integrity sha512-VHZlT21bh/TWnHOQMGret3UcMdJOsyWvagK7MG8rLczYmrPEtvxnJjwPhyrEj1oJC+fz2P//bfQ6gyrD4HrmEQ== +metascraper-audio@^5.11.1: + version "5.11.1" + resolved "https://registry.yarnpkg.com/metascraper-audio/-/metascraper-audio-5.11.1.tgz#46a45fc8d9c4ccc1c24340d46a8c25dc3685d7b9" + integrity sha512-L5eGfw5cOww4/f3ppMa/k+bix3LdICKcKJ2WVTLgz1QkKTWt5IQrgdW+kRfwUdaUTH6w0Tco+nOO7yUCaWytAQ== dependencies: - "@metascraper/helpers" "^5.10.7" + "@metascraper/helpers" "^5.11.1" metascraper-author@^5.10.7: version "5.10.7" diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 3f8724bfb..2a87e3d83 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -342,7 +342,6 @@ Then( Given("I am logged in with these credentials:", table => { loginCredentials = table.hashes()[0]; - cy.debug(); cy.factory().build("user", { ...termsAndConditionsAgreedVersion, name: loginCredentials.email, @@ -420,7 +419,6 @@ When("mention {string} in the text", mention => { cy.get(".suggestion-list__item") .contains(mention) .click(); - cy.debug(); }); Then("the notification gets marked as read", () => { diff --git a/webapp/components/CommentList/CommentList.vue b/webapp/components/CommentList/CommentList.vue index 2e6647c2d..1cff75138 100644 --- a/webapp/components/CommentList/CommentList.vue +++ b/webapp/components/CommentList/CommentList.vue @@ -1,12 +1,12 @@