From 8477e71c28019c1226c9e788a051ec2994ee4006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Apr 2019 01:02:00 +0200 Subject: [PATCH 1/5] Sketch test for #350 --- backend/src/resolvers/notifications.spec.js | 121 ++++++++++++++++++++ backend/src/seed/factories/index.js | 4 +- backend/src/seed/factories/notifications.js | 18 +++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 backend/src/resolvers/notifications.spec.js create mode 100644 backend/src/seed/factories/notifications.js diff --git a/backend/src/resolvers/notifications.spec.js b/backend/src/resolvers/notifications.spec.js new file mode 100644 index 000000000..95b7ac81e --- /dev/null +++ b/backend/src/resolvers/notifications.spec.js @@ -0,0 +1,121 @@ + +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() +let client + +beforeEach(async () => { + await factory.create('User', { + id: 'you', + email: 'test@example.org', + password: '1234' + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('Notification', () => { + const query = `{ + Notification { + id + } + }` + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(query)).rejects.toThrow('Not Authorised') + }) + }) +}) + +describe('currentUser { notifications }', () => { + let variables = {} + + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + describe('given some notifications', () => { + beforeEach(async () => { + const neighborParams = { + email: 'neighbor@example.org', + password: '1234', + id: 'neighbor' + } + await Promise.all([ + factory.create('User', neighborParams), + factory.create('Notification', { id: 'not-for-you' }), + factory.create('Notification', { id: 'already-seen', read: true }) + ]) + await factory.create('Notification', { id: 'unseen' }) + await factory.authenticateAs(neighborParams) + await factory.create('Post', { id: 'p1' }) + await Promise.all([ + factory.relate('Notification', 'User', { from: 'not-for-you', to: 'neighbor'}), + factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you' }), + factory.relate('Notification', 'User', { from: 'unseen', to: 'you'}), + factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen' }), + factory.relate('Notification', 'User', { from: 'already-seen', to: 'you'}), + factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' }), + ]) + }) + + describe('filter for read: false', () => { + const query = `query($read: Boolean) { + currentUser { + notifications(read: $read, orderBy: createdAt_desc) { + id + post { + id + } + } + } + }` + let variables = { read: false } + it('returns only unread notifications of current user', async () => { + const expected = { + currentUser: { + notifications: [ + { id: 'unseen', post: { id: 'p1' } } + ] + } + } + await expect(client.request(query, variables)).resolves.toEqual(expected) + }) + }) + + describe('no filters', () => { + const query = `{ + currentUser { + notifications(orderBy: createdAt_desc) { + id + post { + id + } + } + } + }` + it('returns all notifications of current user', async () => { + const expected = { + currentUser: { + notifications: [ + { id: 'unseen', post: { id: 'p1' } }, + { id: 'already-seen', post: { id: 'p1' } } + ] + } + } + await expect(client.request(query, variables)).resolves.toEqual(expected) + }) + }) + }) + }) +}) diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index e19239ece..7c23226cb 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -8,6 +8,7 @@ import createComment from './comments.js' import createCategory from './categories.js' import createTag from './tags.js' import createReport from './reports.js' +import createNotification from './notifications.js' export const seedServerHost = 'http://127.0.0.1:4001' @@ -29,7 +30,8 @@ const factories = { Comment: createComment, Category: createCategory, Tag: createTag, - Report: createReport + Report: createReport, + Notification: createNotification } export const cleanDatabase = async (options = {}) => { diff --git a/backend/src/seed/factories/notifications.js b/backend/src/seed/factories/notifications.js new file mode 100644 index 000000000..4ec7b61f6 --- /dev/null +++ b/backend/src/seed/factories/notifications.js @@ -0,0 +1,18 @@ +import faker from 'faker' +import uuid from 'uuid/v4' + +export default function (params) { + const { + id = uuid(), + read = false + } = params + + return ` + mutation { + CreateNotification( + id: "${id}", + read: ${read}, + ) { id, read } + } + ` +} From 03d4c93f960dbf50d07dbb8f6bc673766242082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Apr 2019 01:23:33 +0200 Subject: [PATCH 2/5] Add Notification type --- backend/src/middleware/dateTimeMiddleware.js | 4 +++- backend/src/schema.graphql | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/dateTimeMiddleware.js b/backend/src/middleware/dateTimeMiddleware.js index 73b75070c..11b6498a4 100644 --- a/backend/src/middleware/dateTimeMiddleware.js +++ b/backend/src/middleware/dateTimeMiddleware.js @@ -13,9 +13,11 @@ export default { CreatePost: setCreatedAt, CreateComment: setCreatedAt, CreateOrganization: setCreatedAt, + CreateNotification: setCreatedAt, UpdateUser: setUpdatedAt, UpdatePost: setUpdatedAt, UpdateComment: setUpdatedAt, - UpdateOrganization: setUpdatedAt + UpdateOrganization: setUpdatedAt, + UpdateNotification: setUpdatedAt } } diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index 3b1d95a95..5722500ea 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -69,6 +69,14 @@ type Statistics { countShouts: Int! } +type Notification { + id: ID! + read: Boolean, + user: User @relation(name: "NOTIFIED", direction: "OUT") + post: Post @relation(name: "NOTIFIED", direction: "IN") + createdAt: String +} + scalar Date scalar Time scalar DateTime From 97d9b6d7728a23af33f15541df17aa09ee192d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Apr 2019 02:22:34 +0200 Subject: [PATCH 3/5] Add notifications to User --- backend/src/schema.graphql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/schema.graphql b/backend/src/schema.graphql index 5722500ea..db468471a 100644 --- a/backend/src/schema.graphql +++ b/backend/src/schema.graphql @@ -132,6 +132,8 @@ type User { createdAt: String updatedAt: String + notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN") + friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") From bb3a8525db3764e99b3fd6d62606d7f9ab7d2113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Apr 2019 02:23:27 +0200 Subject: [PATCH 4/5] Only admins are allowed to query all notifications --- backend/src/middleware/permissionsMiddleware.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 736ce20a9..495bc9145 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -44,6 +44,7 @@ const isAuthor = rule({ cache: 'no_cache' })(async (parent, args, { user, driver // Permissions const permissions = shield({ Query: { + Notification: isAdmin, statistics: allow, currentUser: allow, Post: or(onlyEnabledContent, isModerator) From 710a8515d3ac55b7930be3c7bc51ee82e7a55fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Apr 2019 11:20:19 +0200 Subject: [PATCH 5/5] Fix lint --- backend/src/resolvers/notifications.spec.js | 13 ++++++------- backend/src/seed/factories/notifications.js | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/src/resolvers/notifications.spec.js b/backend/src/resolvers/notifications.spec.js index 95b7ac81e..50ded7bc4 100644 --- a/backend/src/resolvers/notifications.spec.js +++ b/backend/src/resolvers/notifications.spec.js @@ -36,7 +36,6 @@ describe('Notification', () => { describe('currentUser { notifications }', () => { let variables = {} - describe('authenticated', () => { let headers beforeEach(async () => { @@ -60,12 +59,12 @@ describe('currentUser { notifications }', () => { await factory.authenticateAs(neighborParams) await factory.create('Post', { id: 'p1' }) await Promise.all([ - factory.relate('Notification', 'User', { from: 'not-for-you', to: 'neighbor'}), - factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you' }), - factory.relate('Notification', 'User', { from: 'unseen', to: 'you'}), - factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen' }), - factory.relate('Notification', 'User', { from: 'already-seen', to: 'you'}), - factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' }), + factory.relate('Notification', 'User', { from: 'not-for-you', to: 'neighbor' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'not-for-you' }), + factory.relate('Notification', 'User', { from: 'unseen', to: 'you' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'unseen' }), + factory.relate('Notification', 'User', { from: 'already-seen', to: 'you' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'already-seen' }) ]) }) diff --git a/backend/src/seed/factories/notifications.js b/backend/src/seed/factories/notifications.js index 4ec7b61f6..2e2abdd55 100644 --- a/backend/src/seed/factories/notifications.js +++ b/backend/src/seed/factories/notifications.js @@ -1,4 +1,3 @@ -import faker from 'faker' import uuid from 'uuid/v4' export default function (params) {