diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index fe001d676..8a7aced85 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -24,6 +24,12 @@ const onlyYourself = rule({ return context.user.id === args.id }) +const isMyEmotion = rule({ + cache: 'no_cache', +})(async (parent, args, context, info) => { + return context.user.id === args.from.id +}) + const isMyOwn = rule({ cache: 'no_cache', })(async (parent, args, context, info) => { @@ -184,7 +190,7 @@ const permissions = shield( requestPasswordReset: allow, resetPassword: allow, AddPostEmotions: isAuthenticated, - RemovePostEmotions: isAuthenticated, + RemovePostEmotions: isMyEmotion, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index c4ed494b7..754d9a6ed 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -96,14 +96,15 @@ export default { const transactionRes = await session.run( `MATCH (userFrom:User {id: $from.id})-[emotedRelation:EMOTED {emotion: $data.emotion}]->(postTo:Post {id: $to.id}) DELETE emotedRelation - RETURN emotedRelation`, + `, { from, to, data }, ) session.close() - const [removed] = transactionRes.records.map(record => { - return record.get('emotedRelation') + const [emoted] = transactionRes.records.map(record => { + return record.get('emotedRelation').properties.emotion }) - return !!removed + + return !emoted }, }, } diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index e1c85974b..68de51e79 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -64,6 +64,7 @@ beforeEach(async () => { password: '1234', } authorParams = { + id: 'u25', email: 'author@example.org', password: '1234', } @@ -387,11 +388,23 @@ describe('DeletePost', () => { describe('emotions', () => { let addPostEmotionsVariables - let postEmotionsVariables + let postEmotionsQueryVariables + const postEmotionsQuery = ` + query($id: ID!) { + Post(id: $id) { + emotions { + emotion + User { + id + } + } + } + } + ` beforeEach(async () => { const asAuthor = Factory() authorParams = { - id: 'u25', + id: authorParams.id, email: 'wanna-add-emotions@example.org', password: '1234', } @@ -402,17 +415,18 @@ describe('emotions', () => { title: postTitle, content: postContent, }) - addPostEmotionsVariables = { - from: { id: authorParams.id }, - to: { id: 'p1376' }, - data: { emotion: 'happy' }, - } - postEmotionsVariables = { - id: 'p1376', - } + postEmotionsQueryVariables = { id: 'p1376' } }) describe('AddPostEmotions', () => { + beforeEach(() => { + addPostEmotionsVariables = { + from: { id: authorParams.id }, + to: { id: 'p1376' }, + data: { emotion: 'happy' }, + } + }) + const addPostEmotionsMutation = ` mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { AddPostEmotions(from: $from, to: $to, data: $data) { @@ -429,22 +443,13 @@ describe('emotions', () => { } } ` - const postEmotionsQuery = ` - query($id: ID!) { - Post(id: $id) { - emotions { - emotion - } - } - } - ` describe('unauthenticated', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( client.request(addPostEmotionsMutation, { - from: { id: 'u25' }, + from: { id: authorParams.id }, to: { id: 'p1376' }, data: { emotion: 'happy' }, }), @@ -472,6 +477,7 @@ describe('emotions', () => { client.request(addPostEmotionsMutation, addPostEmotionsVariables), ).resolves.toEqual(expected) }) + it('limits the addition of the same emotion to 1', async () => { const expected = { Post: [ @@ -483,16 +489,21 @@ describe('emotions', () => { await client.request(addPostEmotionsMutation, addPostEmotionsVariables) await client.request(addPostEmotionsMutation, addPostEmotionsVariables) await expect( - client.request(postEmotionsCountQuery, postEmotionsVariables), + client.request(postEmotionsCountQuery, postEmotionsQueryVariables), ).resolves.toEqual(expected) }) it('allows a user to add more than one emotion', async () => { - const expected = [{ emotion: 'happy' }, { emotion: 'surprised' }] + const expected = [ + { emotion: 'happy', User: { id: authorParams.id } }, + { emotion: 'surprised', User: { id: authorParams.id } }, + ] await client.request(addPostEmotionsMutation, addPostEmotionsVariables) addPostEmotionsVariables.data.emotion = 'surprised' await client.request(addPostEmotionsMutation, addPostEmotionsVariables) - await expect(client.request(postEmotionsQuery, postEmotionsVariables)).resolves.toEqual({ + await expect( + client.request(postEmotionsQuery, postEmotionsQueryVariables), + ).resolves.toEqual({ Post: [{ emotions: expect.arrayContaining(expected) }], }) }) @@ -521,22 +532,73 @@ describe('emotions', () => { }) describe('RemovePostEmotions', () => { + let removePostEmotionsVariables const removePostEmotionsMutation = ` mutation($from: _UserInput!, $to: _PostInput!, $data: _EMOTEDInput!) { RemovePostEmotions(from: $from, to: $to, data: $data) } ` + beforeEach(() => { + removePostEmotionsVariables = { + from: { id: authorParams.id }, + to: { id: 'p1376' }, + data: { emotion: 'cry' }, + } + }) + describe('unauthenticated', () => { it('throws authorization error', async () => { client = new GraphQLClient(host) await expect( - client.request(removePostEmotionsMutation, { - from: { id: 'u25' }, - to: { id: 'p1376' }, - data: { emotion: 'happy' }, - }), + client.request(removePostEmotionsMutation, removePostEmotionsVariables), ).rejects.toThrow('Not Authorised') }) }) + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login(authorParams) + client = new GraphQLClient(host, { headers }) + await factory.emote({ + from: authorParams.id, + to: 'p1376', + data: 'cry', + }) + await factory.emote({ + from: authorParams.id, + to: 'p1376', + data: 'happy', + }) + }) + + describe('but not the emoter', () => { + it('throws an authorization error', async () => { + removePostEmotionsVariables.from.id = userParams.id + await expect( + client.request(removePostEmotionsMutation, removePostEmotionsVariables), + ).rejects.toThrow('Not Authorised') + }) + }) + + describe('as the emoter', () => { + it('removes an emotion from a post', async () => { + const expected = { RemovePostEmotions: true } + await expect( + client.request(removePostEmotionsMutation, removePostEmotionsVariables), + ).resolves.toEqual(expected) + }) + + it('removes only the requested emotion, not all emotions', async () => { + const expected = [{ emotion: 'happy', User: { id: authorParams.id } }] + await client.request(removePostEmotionsMutation, removePostEmotionsVariables) + await expect( + client.request(postEmotionsQuery, postEmotionsQueryVariables), + ).resolves.toEqual({ + Post: [{ emotions: expect.arrayContaining(expected) }], + }) + }) + }) + }) }) }) diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index e841b0beb..6dd04f64b 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -130,6 +130,23 @@ export default function Factory(options = {}) { this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) return this }, + async emote({ from, to, data }) { + const mutation = ` + mutation { + AddPostEmotions( + from: { id: "${from}" }, + to: { id: "${to}" }, + data: { emotion: ${data} } + ) { + from { id } + to { id } + emotion + } + } + ` + this.lastResponse = await this.graphQLClient.request(mutation) + return this + }, } result.authenticateAs.bind(result) result.create.bind(result) diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 8f693cfd3..1d63e1958 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -487,6 +487,86 @@ import Factory from './factories' from: 'p15', to: 'Demokratie', }), + f.emote({ + from: 'u1', + to: 'p15', + data: 'surprised', + }), + f.emote({ + from: 'u2', + to: 'p14', + data: 'cry', + }), + f.emote({ + from: 'u3', + to: 'p13', + data: 'angry', + }), + f.emote({ + from: 'u4', + to: 'p12', + data: 'funny', + }), + f.emote({ + from: 'u5', + to: 'p11', + data: 'surprised', + }), + f.emote({ + from: 'u6', + to: 'p10', + data: 'cry', + }), + f.emote({ + from: 'u5', + to: 'p9', + data: 'happy', + }), + f.emote({ + from: 'u4', + to: 'p8', + data: 'angry', + }), + f.emote({ + from: 'u3', + to: 'p7', + data: 'funny', + }), + f.emote({ + from: 'u2', + to: 'p6', + data: 'surprised', + }), + f.emote({ + from: 'u1', + to: 'p5', + data: 'cry', + }), + f.emote({ + from: 'u2', + to: 'p4', + data: 'happy', + }), + f.emote({ + from: 'u3', + to: 'p3', + data: 'angry', + }), + f.emote({ + from: 'u4', + to: 'p2', + data: 'funny', + }), + f.emote({ + from: 'u5', + to: 'p1', + data: 'surprised', + }), + f.emote({ + from: 'u6', + to: 'p0', + data: 'cry', + }), ]) await Promise.all([