diff --git a/backend/src/schema/resolvers/follow.js b/backend/src/schema/resolvers/follow.js index 730a66cfd..f0a98e1f7 100644 --- a/backend/src/schema/resolvers/follow.js +++ b/backend/src/schema/resolvers/follow.js @@ -1,51 +1,44 @@ +import { neode as getNeode } from '../../bootstrap/neo4j' + +const neode = getNeode() + export default { Mutation: { follow: async (_object, params, context, _resolveInfo) => { - const { id, type } = params + const { id: followedId, type } = params + const { user: currentUser } = context - const session = context.driver.session() - const transactionRes = await session.run( - `MATCH (node {id: $id}), (user:User {id: $userId}) - WHERE $type IN labels(node) AND NOT $id = $userId - MERGE (user)-[relation:FOLLOWS]->(node) - RETURN COUNT(relation) > 0 as isFollowed`, - { - id, - type, - userId: context.user.id, - }, - ) + if (type === 'User' && currentUser.id === followedId) { + return null + } - const [isFollowed] = transactionRes.records.map(record => { - return record.get('isFollowed') - }) - - session.close() - - return isFollowed + const [user, followedNode] = await Promise.all([ + neode.find('User', currentUser.id), + neode.find(type, followedId), + ]) + await user.relateTo(followedNode, 'following') + return followedNode.toJson() }, unfollow: async (_object, params, context, _resolveInfo) => { - const { id, type } = params - const session = context.driver.session() + const { id: followedId, type } = params + const { user: currentUser } = context - const transactionRes = await session.run( - `MATCH (user:User {id: $userId})-[relation:FOLLOWS]->(node {id: $id}) + /* + * Note: Neode doesn't provide an easy method for retrieving or removing relationships. + * It's suggested to use query builder feature (https://github.com/adam-cowley/neode/issues/67) + * However, pure cypher query looks cleaner IMO + */ + await neode.cypher( + `MATCH (user:User {id: $currentUser.id})-[relation:FOLLOWS]->(node {id: $followedId}) WHERE $type IN labels(node) DELETE relation RETURN COUNT(relation) > 0 as isFollowed`, - { - id, - type, - userId: context.user.id, - }, + { followedId, type, currentUser }, ) - const [isFollowed] = transactionRes.records.map(record => { - return record.get('isFollowed') - }) - session.close() - return isFollowed + const followedNode = await neode.find(type, followedId) + return followedNode.toJson() }, }, } diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index 66be20841..1aa146075 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -1,36 +1,93 @@ -import { GraphQLClient } from 'graphql-request' +import { createTestClient } from 'apollo-server-testing' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { getDriver } from '../../bootstrap/neo4j' +import createServer from '../../server' +import { gql } from '../../jest/helpers' const factory = Factory() -let clientUser1 -let headersUser1 +const driver = getDriver() -const mutationFollowUser = id => ` - mutation { - follow(id: "${id}", type: User) +let query +let mutate +let authenticatedUser + +let user1 +let user2 +let variables + +const mutationFollowUser = gql` + mutation($id: ID!, $type: FollowTypeEnum) { + follow(id: $id, type: $type) { + name + followedBy { + id + name + } + followedByCurrentUser + } } ` -const mutationUnfollowUser = id => ` - mutation { - unfollow(id: "${id}", type: User) + +const mutationUnfollowUser = gql` + mutation($id: ID!, $type: FollowTypeEnum) { + unfollow(id: $id, type: $type) { + name + followedBy { + id + name + } + followedByCurrentUser + } } ` +const userQuery = gql` + query($id: ID) { + User(id: $id) { + followedBy { + id + } + followedByCurrentUser + } + } +` + +beforeAll(() => { + const { server } = createServer({ + context: () => ({ + driver, + user: authenticatedUser, + cypherParams: { + currentUserId: authenticatedUser ? authenticatedUser.id : null, + }, + }), + }) + + const testClient = createTestClient(server) + query = testClient.query + mutate = testClient.mutate +}) + beforeEach(async () => { - await factory.create('User', { - id: 'u1', - email: 'test@example.org', - password: '1234', - }) - await factory.create('User', { - id: 'u2', - email: 'test2@example.org', - password: '1234', - }) + user1 = await factory + .create('User', { + id: 'u1', + name: 'user1', + email: 'test@example.org', + password: '1234', + }) + .then(user => user.toJson()) + user2 = await factory + .create('User', { + id: 'u2', + name: 'user2', + email: 'test2@example.org', + password: '1234', + }) + .then(user => user.toJson()) - headersUser1 = await login({ email: 'test@example.org', password: '1234' }) - clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) + authenticatedUser = user1 + variables = { id: user2.id, type: 'User' } }) afterEach(async () => { @@ -40,84 +97,73 @@ afterEach(async () => { describe('follow', () => { describe('follow user', () => { describe('unauthenticated follow', () => { - it('throws authorization error', async () => { - const client = new GraphQLClient(host) - await expect(client.request(mutationFollowUser('u2'))).rejects.toThrow('Not Authorised') + test('throws authorization error', async () => { + authenticatedUser = null + const { errors, data } = await mutate({ + mutation: mutationFollowUser, + variables, + }) + expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(data).toMatchObject({ follow: null }) }) }) - it('I can follow another user', async () => { - const res = await clientUser1.request(mutationFollowUser('u2')) - const expected = { - follow: true, - } - expect(res).toMatchObject(expected) - - const { User } = await clientUser1.request(`{ - User(id: "u2") { - followedBy { id } - followedByCurrentUser - } - }`) - const expected2 = { - followedBy: [{ id: 'u1' }], + test('I can follow another user', async () => { + const { data: result } = await mutate({ + mutation: mutationFollowUser, + variables, + }) + const expectedUser = { + name: user2.name, + followedBy: [{ id: user1.id, name: user1.name }], followedByCurrentUser: true, } - expect(User[0]).toMatchObject(expected2) + expect(result).toMatchObject({ follow: expectedUser }) }) - it('I can`t follow myself', async () => { - const res = await clientUser1.request(mutationFollowUser('u1')) - const expected = { - follow: false, - } - expect(res).toMatchObject(expected) + test('I can`t follow myself', async () => { + variables.id = user1.id + const { data: result } = await mutate({ mutation: mutationFollowUser, variables }) + const expectedResult = { follow: null } + expect(result).toMatchObject(expectedResult) - const { User } = await clientUser1.request(`{ - User(id: "u1") { - followedBy { id } - followedByCurrentUser - } - }`) - const expected2 = { + const { data } = await query({ + query: userQuery, + variables: { id: user1.id }, + }) + const expectedUser = { followedBy: [], followedByCurrentUser: false, } - expect(User[0]).toMatchObject(expected2) + expect(data).toMatchObject({ User: [expectedUser] }) }) }) describe('unfollow user', () => { + beforeEach(async () => { + variables = { + id: user2.id, + type: 'User', + } + await mutate({ mutation: mutationFollowUser, variables }) + }) + describe('unauthenticated follow', () => { - it('throws authorization error', async () => { - // follow - await clientUser1.request(mutationFollowUser('u2')) - // unfollow - const client = new GraphQLClient(host) - await expect(client.request(mutationUnfollowUser('u2'))).rejects.toThrow('Not Authorised') + test('throws authorization error', async () => { + authenticatedUser = null + const { errors, data } = await mutate({ mutation: mutationUnfollowUser, variables }) + expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(data).toMatchObject({ unfollow: null }) }) }) it('I can unfollow a user', async () => { - // follow - await clientUser1.request(mutationFollowUser('u2')) - // unfollow - const expected = { - unfollow: true, - } - const res = await clientUser1.request(mutationUnfollowUser('u2')) - expect(res).toMatchObject(expected) - - const { User } = await clientUser1.request(`{ - User(id: "u2") { - followedBy { id } - followedByCurrentUser - } - }`) - const expected2 = { + const { data: result } = await mutate({ mutation: mutationUnfollowUser, variables }) + const expectedUser = { + name: user2.name, followedBy: [], followedByCurrentUser: false, } - expect(User[0]).toMatchObject(expected2) + expect(result).toMatchObject({ unfollow: expectedUser }) }) }) }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index c641763f0..620039b28 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -32,9 +32,9 @@ type Mutation { # Unshout the given Type and ID unshout(id: ID!, type: ShoutTypeEnum): Boolean! # Follow the given Type and ID - follow(id: ID!, type: FollowTypeEnum): Boolean! + follow(id: ID!, type: FollowTypeEnum): User # Unfollow the given Type and ID - unfollow(id: ID!, type: FollowTypeEnum): Boolean! + unfollow(id: ID!, type: FollowTypeEnum): User } type Report { diff --git a/webapp/components/FollowButton.vue b/webapp/components/FollowButton.vue index cc4662c85..5275035e5 100644 --- a/webapp/components/FollowButton.vue +++ b/webapp/components/FollowButton.vue @@ -15,7 +15,7 @@