diff --git a/backend/src/graphql-schema.js b/backend/src/graphql-schema.js index 57b2ffb6c..c17b967d2 100644 --- a/backend/src/graphql-schema.js +++ b/backend/src/graphql-schema.js @@ -7,6 +7,7 @@ import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' import moderation from './resolvers/moderation.js' import rewards from './resolvers/rewards.js' +import notifications from './resolvers/notifications' export const typeDefs = fs .readFileSync( @@ -17,13 +18,15 @@ export const typeDefs = fs export const resolvers = { Query: { ...statistics.Query, - ...userManagement.Query + ...userManagement.Query, + ...notifications.Query }, Mutation: { ...userManagement.Mutation, ...reports.Mutation, ...posts.Mutation, ...moderation.Mutation, - ...rewards.Mutation + ...rewards.Mutation, + ...notifications.Mutation } } diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 495bc9145..4ff334806 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -20,6 +20,21 @@ const isMyOwn = rule({ cache: 'no_cache' })(async (parent, args, context, info) return context.user.id === parent.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) +}) + const onlyEnabledContent = rule({ cache: 'strict' })(async (parent, args, ctx, info) => { const { disabled, deleted } = args return !(disabled || deleted) @@ -50,6 +65,7 @@ const permissions = shield({ Post: or(onlyEnabledContent, isModerator) }, Mutation: { + UpdateNotification: belongsToMe, CreatePost: isAuthenticated, UpdatePost: isAuthor, DeletePost: isAuthor, diff --git a/backend/src/resolvers/notifications.js b/backend/src/resolvers/notifications.js new file mode 100644 index 000000000..bc3da0acf --- /dev/null +++ b/backend/src/resolvers/notifications.js @@ -0,0 +1,14 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' + +export default { + Query: { + Notification: (object, params, context, resolveInfo) => { + return neo4jgraphql(object, params, context, resolveInfo, false) + } + }, + Mutation: { + UpdateNotification: (object, params, context, resolveInfo) => { + return neo4jgraphql(object, params, context, resolveInfo, false) + } + } +} diff --git a/backend/src/resolvers/notifications.spec.js b/backend/src/resolvers/notifications.spec.js index 50ded7bc4..799bc1594 100644 --- a/backend/src/resolvers/notifications.spec.js +++ b/backend/src/resolvers/notifications.spec.js @@ -5,13 +5,14 @@ import { host, login } from '../jest/helpers' const factory = Factory() let client +let userParams = { + id: 'you', + email: 'test@example.org', + password: '1234' +} beforeEach(async () => { - await factory.create('User', { - id: 'you', - email: 'test@example.org', - password: '1234' - }) + await factory.create('User', userParams) }) afterEach(async () => { @@ -118,3 +119,63 @@ describe('currentUser { notifications }', () => { }) }) }) + +describe('UpdateNotification', () => { + const mutation = `mutation($id: ID!, $read: Boolean){ + UpdateNotification(id: $id, read: $read) { + id read + } + }` + const variables = { id: 'to-be-updated', read: true } + + describe('given a notifications', () => { + let headers + + beforeEach(async () => { + const mentionedParams = { + id: 'mentioned-1', + email: 'mentioned@example.org', + password: '1234', + slug: 'mentioned' + } + await factory.create('User', mentionedParams) + await factory.create('Notification', { id: 'to-be-updated' }) + await factory.authenticateAs(userParams) + await factory.create('Post', { id: 'p1' }) + await Promise.all([ + factory.relate('Notification', 'User', { from: 'to-be-updated', to: 'mentioned-1' }), + factory.relate('Notification', 'Post', { from: 'p1', to: 'to-be-updated' }) + ]) + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('throws authorization error', async () => { + await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') + }) + + describe('and owner', () => { + beforeEach(async () => { + headers = await login({ email: 'mentioned@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + it('updates notification', async () => { + const expected = { UpdateNotification: { id: 'to-be-updated', read: true } } + await expect(client.request(mutation, variables)).resolves.toEqual(expected) + }) + }) + }) + }) +}) diff --git a/backend/src/server.js b/backend/src/server.js index efa9a17c0..fe0d4ee1d 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -28,10 +28,10 @@ let schema = makeAugmentedSchema({ resolvers, config: { query: { - exclude: ['Statistics', 'LoggedInUser'] + exclude: ['Notfication', 'Statistics', 'LoggedInUser'] }, mutation: { - exclude: ['Statistics', 'LoggedInUser'] + exclude: ['Notfication', 'Statistics', 'LoggedInUser'] }, debug: debug }