From 2846a6a8d3a09c23256e82c96120fe6cd8b264e8 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Thu, 29 Aug 2019 21:56:56 +0200 Subject: [PATCH] Spec for notifications passing --- .../src/middleware/permissionsMiddleware.js | 28 +- backend/src/schema/resolvers/notifications.js | 32 +- .../schema/resolvers/notifications.spec.js | 388 ++++++++---------- backend/src/schema/types/type/NOTIFIED.gql | 11 +- 4 files changed, 205 insertions(+), 254 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index b40e7bc99..2a52e54af 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -41,32 +41,6 @@ const isMySocialMedia = rule({ return socialMedia.ownedBy.node.id === user.id }) -const belongsToMe = rule({ - cache: 'no_cache', -})(async (_, args, context) => { - const { - driver, - user: { id: userId }, - } = context - const { id: notificationId } = args - const session = driver.session() - const result = await session.run( - ` - MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId}) - RETURN n - `, - { - userId, - notificationId, - }, - ) - const [notification] = result.records.map(record => { - return record.get('n') - }) - session.close() - return Boolean(notification) -}) - /* TODO: decide if we want to remove this check: the check * `onlyEnabledContent` throws authorization errors only if you have * arguments for `disabled` or `deleted` assuming these are filter @@ -197,7 +171,7 @@ const permissions = shield( RemovePostEmotions: isAuthenticated, block: isAuthenticated, unblock: isAuthenticated, - markAsRead: belongsToMe, + markAsRead: isAuthenticated, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/notifications.js b/backend/src/schema/resolvers/notifications.js index 6711f6fdd..e46b61ba4 100644 --- a/backend/src/schema/resolvers/notifications.js +++ b/backend/src/schema/resolvers/notifications.js @@ -56,8 +56,36 @@ export default { }, }, Mutation: { - markAsRead: async (parent, params, context, resolveInfo) => { - return null + markAsRead: async (parent, args, context, resolveInfo) => { + const { user } = context + const session = context.driver.session() + let notification + try { + const cypher = ` + MATCH (resource {id: $resourceId})-[notification:NOTIFIED]->(user:User {id:$id}) + SET notification.read = TRUE + RETURN resource, notification, user + ` + const result = await session.run(cypher, { resourceId: args.id, id: user.id }) + const resourceTypes = ['Post', 'Comment'] + const notifications = await result.records.map(record => { + return { + ...record.get('notification').properties, + from: { + __typename: record.get('resource').labels.find(l => resourceTypes.includes(l)), + ...record.get('resource').properties, + }, + to: { + __typename: 'User', + ...record.get('user').properties, + }, + } + }) + notification = notifications[0] + } finally { + session.close() + } + return notification }, }, } diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 72a9a8355..e11c9a73e 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,11 +1,9 @@ -import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login, gql } from '../../jest/helpers' +import { gql } from '../../jest/helpers' import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../.././server' -let client const factory = Factory() const neode = getNeode() const driver = getDriver() @@ -16,6 +14,7 @@ const userParams = { } let authenticatedUser +let user let variables let query let mutate @@ -42,90 +41,82 @@ afterEach(async () => { await factory.cleanDatabase() }) -describe('notifications', () => { - const notificationQuery = gql` - query($read: Boolean, $orderBy: NOTIFIEDOrdering) { - notifications(read: $read, orderBy: $orderBy) { - from { - __typename - ... on Post { - content - } - ... on Comment { - content - } - } - read - createdAt - } - } - ` - +describe('given some notifications', () => { + beforeEach(async () => { + user = await factory.create('User', userParams) + await factory.create('User', { id: 'neighbor' }) + await Promise.all(setupNotifications.map(s => neode.cypher(s))) + }) const setupNotifications = [ - ` -MATCH(user:User {id: 'neighbor'}) -MERGE (:Post {id: 'p1', content: 'Not for you'}) - -[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}] - ->(user); -`, - ` -MATCH(user:User {id: 'you'}) -MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'}) - -[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}] - ->(user); -`, - ` -MATCH(user:User {id: 'you'}) -MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'}) - -[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}] - ->(user); -`, - ` -MATCH(user:User {id: 'you'}) -MATCH(post:Post {id: 'p3'}) -CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'}) -MERGE (comment)-[:COMMENTS]->(post) -MERGE (comment) - -[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}] - ->(user); -`, - ` -MATCH(user:User {id: 'you'}) -MATCH(post:Post {id: 'p3'}) -CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'}) -MERGE (comment)-[:COMMENTS]->(post) -MERGE (comment) - -[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}] - ->(user); -`, - ` -MATCH(user:User {id: 'neighbor'}) -MATCH(post:Post {id: 'p3'}) -CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'}) -MERGE (comment)-[:COMMENTS]->(post) -MERGE (comment) - -[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}] - ->(user); + `MATCH(user:User {id: 'neighbor'}) + MERGE (:Post {id: 'p1', content: 'Not for you'}) + -[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}] + ->(user); + `, + `MATCH(user:User {id: 'you'}) + MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'}) + -[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}] + ->(user); + `, + `MATCH(user:User {id: 'you'}) + MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'}) + -[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}] + ->(user); + `, + `MATCH(user:User {id: 'you'}) + MATCH(post:Post {id: 'p3'}) + CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'}) + MERGE (comment)-[:COMMENTS]->(post) + MERGE (comment) + -[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}] + ->(user); + `, + `MATCH(user:User {id: 'you'}) + MATCH(post:Post {id: 'p3'}) + CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'}) + MERGE (comment)-[:COMMENTS]->(post) + MERGE (comment) + -[:NOTIFIED {createdAt: "2019-08-30T19:33:48.651Z", read: false, reason: "mentioned_in_comment"}] + ->(user); + `, + `MATCH(user:User {id: 'neighbor'}) + MATCH(post:Post {id: 'p3'}) + CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'}) + MERGE (comment)-[:COMMENTS]->(post) + MERGE (comment) + -[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}] + ->(user); `, ] - describe('unauthenticated', () => { - it('throws authorization error', async () => { - const result = await query({ query: notificationQuery }) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - const user = await factory.create('User', userParams) - authenticatedUser = await user.toJson() + describe('notifications', () => { + const notificationQuery = gql` + query($read: Boolean, $orderBy: NotificationOrdering) { + notifications(read: $read, orderBy: $orderBy) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } + } + ` + describe('unauthenticated', () => { + it('throws authorization error', async () => { + const result = await query({ query: notificationQuery }) + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + }) }) - describe('given some notifications', () => { + describe('authenticated', () => { beforeEach(async () => { - await factory.create('User', { id: 'neighbor' }) - await Promise.all(setupNotifications.map(s => neode.cypher(s))) + authenticatedUser = await user.toJson() }) describe('no filters', () => { @@ -155,7 +146,7 @@ MERGE (comment) content: 'You have been mentioned in a comment', }, read: false, - createdAt: '2019-08-31T17:33:48.651Z', + createdAt: '2019-08-30T19:33:48.651Z', }, { from: { @@ -173,179 +164,130 @@ MERGE (comment) }) describe('filter for read: false', () => { - const queryCurrentUserNotificationsFilterRead = gql` - query($read: Boolean) { - notifications(read: $read, orderBy: createdAt_desc) { - id - post { - id - } - comment { - id - } - } - } - ` - it('returns only unread notifications of current user', async () => { - const expected = { - currentUser: { - notifications: expect.arrayContaining([ + const expected = expect.objectContaining({ + data: { + notifications: [ { - id: 'post-mention-unseen', - post: { - id: 'p1', + from: { + __typename: 'Comment', + content: 'You have been mentioned in a comment', }, - comment: null, + read: false, + createdAt: '2019-08-30T19:33:48.651Z', }, { - id: 'comment-mention-unseen', - post: null, - comment: { - id: 'c1', + from: { + __typename: 'Post', + content: 'You have been mentioned in a post', }, + read: false, + createdAt: '2019-08-31T17:33:48.651Z', }, - ]), + ], }, - } + }) await expect( - client.request(queryCurrentUserNotificationsFilterRead, variables), + query({ query: notificationQuery, variables: { ...variables, read: false } }), ).resolves.toEqual(expected) }) }) }) }) -}) -describe('UpdateNotification', () => { - const mutationUpdateNotification = gql` - mutation($id: ID!, $read: Boolean) { - UpdateNotification(id: $id, read: $read) { - id - read + describe('markAsRead', () => { + const markAsReadMutation = gql` + mutation($id: ID!) { + markAsRead(id: $id) { + from { + __typename + ... on Post { + content + } + ... on Comment { + content + } + } + read + createdAt + } } - } - ` - const variablesPostUpdateNotification = { - id: 'post-mention-to-be-updated', - read: true, - } - const variablesCommentUpdateNotification = { - id: 'comment-mention-to-be-updated', - read: true, - } - - describe('given some notifications', () => { - let headers - - beforeEach(async () => { - const mentionedParams = { - id: 'mentioned-1', - email: 'mentioned@example.org', - password: '1234', - slug: 'mentioned', - } - await Promise.all([ - factory.create('User', mentionedParams), - factory.create('Notification', { - id: 'post-mention-to-be-updated', - reason: 'mentioned_in_post', - }), - factory.create('Notification', { - id: 'comment-mention-to-be-updated', - reason: 'mentioned_in_comment', - }), - ]) - await factory.authenticateAs(userParams) - await factory.create('Post', { id: 'p1', categoryIds }) - await Promise.all([ - factory.relate('Notification', 'User', { - from: 'post-mention-to-be-updated', - to: 'mentioned-1', - }), - factory.relate('Notification', 'Post', { - from: 'p1', - to: 'post-mention-to-be-updated', - }), - ]) - // Comment and its notifications - await Promise.all([ - factory.create('Comment', { - id: 'c1', - postId: 'p1', - }), - ]) - await Promise.all([ - factory.relate('Notification', 'User', { - from: 'comment-mention-to-be-updated', - to: 'mentioned-1', - }), - factory.relate('Notification', 'Comment', { - from: 'p1', - to: 'comment-mention-to-be-updated', - }), - ]) - }) - + ` describe('unauthenticated', () => { it('throws authorization error', async () => { - client = new GraphQLClient(host) - await expect( - client.request(mutationUpdateNotification, variablesPostUpdateNotification), - ).rejects.toThrow('Not Authorised') + const result = await mutate({ + mutation: markAsReadMutation, + variables: { ...variables, id: 'p1' }, + }) + expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') }) }) describe('authenticated', () => { beforeEach(async () => { - headers = await login({ - email: 'test@example.org', - password: '1234', - }) - client = new GraphQLClient(host, { - headers, - }) + authenticatedUser = await user.toJson() }) - it('throws authorization error', async () => { - await expect( - client.request(mutationUpdateNotification, variablesPostUpdateNotification), - ).rejects.toThrow('Not Authorised') - }) - - describe('and owner', () => { + describe('not being notified at all', () => { beforeEach(async () => { - headers = await login({ - email: 'mentioned@example.org', - password: '1234', + variables = { + ...variables, + id: 'p1', + } + }) + + it('returns null', async () => { + const response = await mutate({ mutation: markAsReadMutation, variables }) + expect(response.data.markAsRead).toEqual(null) + expect(response.errors).toBeUndefined() + }) + }) + + describe('being notified', () => { + describe('on a post', () => { + beforeEach(async () => { + variables = { + ...variables, + id: 'p3', + } }) - client = new GraphQLClient(host, { - headers, + + it('updates `read` attribute and returns NOTIFIED relationship', async () => { + const { data } = await mutate({ mutation: markAsReadMutation, variables }) + expect(data).toEqual({ + markAsRead: { + from: { + __typename: 'Post', + content: 'You have been mentioned in a post', + }, + read: true, + createdAt: '2019-08-31T17:33:48.651Z', + }, + }) }) }) - it('updates post notification', async () => { - const expected = { - UpdateNotification: { - id: 'post-mention-to-be-updated', - read: true, - }, - } - await expect( - client.request(mutationUpdateNotification, variablesPostUpdateNotification), - ).resolves.toEqual(expected) - }) + describe('on a comment', () => { + beforeEach(async () => { + variables = { + ...variables, + id: 'c2', + } + }) - it('updates comment notification', async () => { - const expected = { - UpdateNotification: { - id: 'comment-mention-to-be-updated', - read: true, - }, - } - await expect( - client.request(mutationUpdateNotification, variablesCommentUpdateNotification), - ).resolves.toEqual(expected) + it('updates `read` attribute and returns NOTIFIED relationship', async () => { + const { data } = await mutate({ mutation: markAsReadMutation, variables }) + expect(data).toEqual({ + markAsRead: { + from: { + __typename: 'Comment', + content: 'You have been mentioned in a comment', + }, + read: true, + createdAt: '2019-08-30T19:33:48.651Z', + }, + }) + }) }) }) }) diff --git a/backend/src/schema/types/type/NOTIFIED.gql b/backend/src/schema/types/type/NOTIFIED.gql index 0be774f30..965ee56d6 100644 --- a/backend/src/schema/types/type/NOTIFIED.gql +++ b/backend/src/schema/types/type/NOTIFIED.gql @@ -3,17 +3,24 @@ type NOTIFIED { to: User createdAt: String read: Boolean + reason: NotificationReason } union NotificationSource = Post | Comment -enum NOTIFIEDOrdering { +enum NotificationOrdering { createdAt_asc createdAt_desc } +enum NotificationReason { + mentioned_in_post + mentioned_in_comment + comment_on_post +} + type Query { - notifications(read: Boolean, orderBy: NOTIFIEDOrdering): [NOTIFIED] + notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED] } type Mutation {