From bf352dca92a9cc2062c45e6e41178ffbbd7d87a4 Mon Sep 17 00:00:00 2001 From: Matt Rider Date: Thu, 6 Jun 2019 17:28:52 -0300 Subject: [PATCH] Start backend implementation UserAccount Deletion --- .../src/middleware/permissionsMiddleware.js | 7 + backend/src/schema/resolvers/users.js | 28 ++++ backend/src/schema/resolvers/users.spec.js | 152 +++++++++++++++++- backend/src/schema/types/schema.gql | 18 ++- 4 files changed, 196 insertions(+), 9 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index bc9b4c525..7bc2e8caa 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -80,6 +80,12 @@ const isAuthor = rule({ return authorId === user.id }) +const isDeletingOwnAccount = rule({ + cache: 'no_cache', +})(async (parent, args, context, info) => { + return context.user.id === args.id +}) + // Permissions const permissions = shield({ Query: { @@ -115,6 +121,7 @@ const permissions = shield({ CreateComment: isAuthenticated, DeleteComment: isAuthor, // CreateUser: allow, + DeleteUser: isDeletingOwnAccount, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 53bf0967e..d46664c60 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -11,5 +11,33 @@ export default { params = await fileUpload(params, { file: 'avatarUpload', url: 'avatar' }) return neo4jgraphql(object, params, context, resolveInfo, false) }, + DeleteUser: async (object, params, context, resolveInfo) => { + const { comments, posts } = params + const session = context.driver.session() + if (comments) { + await session.run( + ` + MATCH (comments:Comment)<-[:WROTE]-(author:User {id: $userId}) + DETACH DELETE comments + RETURN author`, + { + userId: context.user.id, + }, + ) + } + if (posts) { + await session.run( + ` + MATCH (posts:Post)<-[:WROTE]-(author:User {id: $userId}) + DETACH DELETE posts + RETURN author`, + { + userId: context.user.id, + }, + ) + } + session.close() + return neo4jgraphql(object, params, context, resolveInfo, false) + }, }, } diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index a5c50f4f9..a2b59c19d 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,6 +1,7 @@ import { GraphQLClient } from 'graphql-request' -import { host } from '../../jest/helpers' +import { host, login } from '../../jest/helpers' import Factory from '../../seed/factories' +import gql from 'graphql-tag' const factory = Factory() let client @@ -82,4 +83,153 @@ describe('users', () => { await expect(client.request(mutation, variables)).rejects.toThrow(expected) }) }) + + describe('DeleteUser', () => { + let deleteUserVariables + let asAuthor + const deleteUserMutation = gql` + mutation($id: ID!, $comments: Boolean, $posts: Boolean) { + DeleteUser(id: $id, comments: $comments, posts: $posts) { + id + } + } + ` + beforeEach(async () => { + asAuthor = await factory.create('User', { + email: 'test@example.org', + password: '1234', + id: 'u343', + }) + await factory.create('User', { + email: 'friendsAccount@example.org', + password: '1234', + id: 'u565', + }) + deleteUserVariables = { id: 'u343' } + }) + + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ + email: 'test@example.org', + password: '1234', + }) + client = new GraphQLClient(host, { headers }) + }) + + describe("attempting to delete another user's account", () => { + it('throws an authorization error', async () => { + deleteUserVariables = { id: 'u565' } + await expect(client.request(deleteUserMutation, deleteUserVariables)).rejects.toThrow( + 'Not Authorised', + ) + }) + }) + + describe('attempting to delete my own account', () => { + const commentQuery = gql` + query($content: String) { + Comment(content: $content) { + id + } + } + ` + + const postQuery = gql` + query($content: String) { + Post(content: $content) { + id + } + } + ` + + beforeEach(async () => { + await asAuthor.authenticateAs({ + email: 'test@example.org', + password: '1234', + }) + await asAuthor.create('Post', { + id: 'p139', + content: 'Post by user u343', + }) + await asAuthor.create('Comment', { + id: 'c155', + postId: 'p139', + content: 'Comment by user u343', + }) + deleteUserVariables = { id: 'u343' } + }) + it('deletes my account', async () => { + const expected = { + DeleteUser: { + id: 'u343', + }, + } + await expect(client.request(deleteUserMutation, deleteUserVariables)).resolves.toEqual( + expected, + ) + }) + + describe("doesn't delete a user's", () => { + it('comments by default', async () => { + await client.request(deleteUserMutation, deleteUserVariables) + const commentQueryVariablesByContent = { + content: 'Comment by user u343', + } + const { Comment } = await client.request(commentQuery, commentQueryVariablesByContent) + expect(Comment).toEqual([ + { + id: 'c155', + }, + ]) + }) + + it('posts by default', async () => { + await client.request(deleteUserMutation, deleteUserVariables) + const postQueryVariablesByContent = { + content: 'Post by user u343', + } + const { Post } = await client.request(postQuery, postQueryVariablesByContent) + expect(Post).toEqual([ + { + id: 'p139', + }, + ]) + }) + }) + + describe("deletes a user's", () => { + it('comments on request', async () => { + deleteUserVariables = { id: 'u343', comments: true } + await client.request(deleteUserMutation, deleteUserVariables) + const commentQueryVariablesByContent = { + content: 'Comment by user u343', + } + const { Comment } = await client.request(commentQuery, commentQueryVariablesByContent) + expect(Comment).toEqual([]) + }) + + it('posts on request', async () => { + deleteUserVariables = { id: 'u343', posts: true } + await client.request(deleteUserMutation, deleteUserVariables) + const postQueryVariablesByContent = { + content: 'Post by user u343', + } + const { Post } = await client.request(postQuery, postQueryVariablesByContent) + expect(Post).toEqual([]) + }) + }) + }) + }) + }) }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index ab8b25399..1b9517c52 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -4,8 +4,9 @@ type Query { currentUser: User # Get the latest Network Statistics statistics: Statistics! - findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( - statement: """ + findPosts(filter: String!, limit: Int = 10): [Post]! + @cypher( + statement: """ CALL db.index.fulltext.queryNodes('full_text_search', $filter) YIELD node as post, score MATCH (post)<-[:WROTE]-(user:User) @@ -14,8 +15,8 @@ type Query { AND NOT post.deleted = true AND NOT post.disabled = true RETURN post LIMIT $limit - """ - ) + """ + ) CommentByPost(postId: ID!): [Comment]! } @@ -23,7 +24,7 @@ type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! - changePassword(oldPassword:String!, newPassword: String!): String! + changePassword(oldPassword: String!, newPassword: String!): String! report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID @@ -37,6 +38,7 @@ type Mutation { follow(id: ID!, type: FollowTypeEnum): Boolean! # Unfollow the given Type and ID unfollow(id: ID!, type: FollowTypeEnum): Boolean! + DeleteUser(id: ID!, comments: Boolean, posts: Boolean): User } type Statistics { @@ -53,7 +55,7 @@ type Statistics { type Notification { id: ID! - read: Boolean, + read: Boolean user: User @relation(name: "NOTIFIED", direction: "OUT") post: Post @relation(name: "NOTIFIED", direction: "IN") createdAt: String @@ -80,7 +82,8 @@ type Report { id: ID! submitter: User @relation(name: "REPORTED", direction: "IN") description: String - type: String! @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") + type: String! + @cypher(statement: "MATCH (resource)<-[:REPORTED]-(this) RETURN labels(resource)[0]") createdAt: String comment: Comment @relation(name: "REPORTED", direction: "OUT") post: Post @relation(name: "REPORTED", direction: "OUT") @@ -131,4 +134,3 @@ type SocialMedia { url: String ownedBy: [User]! @relation(name: "OWNED", direction: "IN") } -