diff --git a/src/graphql-schema.js b/src/graphql-schema.js index c2d96ce16..6832d2a7c 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -6,9 +6,11 @@ import reports from './resolvers/reports.js' import posts from './resolvers/posts.js' import moderation from './resolvers/moderation.js' -export const typeDefs = - fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql')) - .toString('utf-8') +export const typeDefs = fs + .readFileSync( + process.env.GRAPHQL_SCHEMA || path.join(__dirname, 'schema.graphql') + ) + .toString('utf-8') export const resolvers = { Query: { diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 434b97200..9e1bc6fbe 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -58,8 +58,14 @@ const permissions = shield({ DeleteBadge: isAdmin, enable: isModerator, disable: isModerator, - changePassword: isAuthenticated // addFruitToBasket: isAuthenticated + follow: isAuthenticated, + unfollow: isAuthenticated, + shout: isAuthenticated, + unshout: isAuthenticated, + changePassword: isAuthenticated, + enable: isModerator, + disable: isModerator // CreateUser: allow, }, User: { diff --git a/src/middleware/sluggifyMiddleware.js b/src/middleware/sluggifyMiddleware.js index 1a9177daa..c94feb55e 100644 --- a/src/middleware/sluggifyMiddleware.js +++ b/src/middleware/sluggifyMiddleware.js @@ -1,12 +1,14 @@ import uniqueSlug from './slugify/uniqueSlug' const isUniqueFor = (context, type) => { - return async (slug) => { + return async slug => { const session = context.driver.session() const response = await session.run( - `MATCH(p:${type} {slug: $slug }) return p.slug`, { + `MATCH(p:${type} {slug: $slug }) return p.slug`, + { slug - }) + } + ) session.close() return response.records.length === 0 } @@ -15,19 +17,27 @@ const isUniqueFor = (context, type) => { export default { Mutation: { CreatePost: async (resolve, root, args, context, info) => { - args.slug = args.slug || await uniqueSlug(args.title, isUniqueFor(context, 'Post')) + args.slug = + args.slug || + (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) return resolve(root, args, context, info) }, CreateUser: async (resolve, root, args, context, info) => { - args.slug = args.slug || await uniqueSlug(args.name, isUniqueFor(context, 'User')) + args.slug = + args.slug || + (await uniqueSlug(args.name, isUniqueFor(context, 'User'))) return resolve(root, args, context, info) }, CreateOrganization: async (resolve, root, args, context, info) => { - args.slug = args.slug || await uniqueSlug(args.name, isUniqueFor(context, 'Organization')) + args.slug = + args.slug || + (await uniqueSlug(args.name, isUniqueFor(context, 'Organization'))) return resolve(root, args, context, info) }, CreateCategory: async (resolve, root, args, context, info) => { - args.slug = args.slug || await uniqueSlug(args.name, isUniqueFor(context, 'Category')) + args.slug = + args.slug || + (await uniqueSlug(args.name, isUniqueFor(context, 'Category'))) return resolve(root, args, context, info) } } diff --git a/src/middleware/slugifyMiddleware.spec.js b/src/middleware/slugifyMiddleware.spec.js index c7cd9806f..bd524e0e4 100644 --- a/src/middleware/slugifyMiddleware.spec.js +++ b/src/middleware/slugifyMiddleware.spec.js @@ -8,7 +8,10 @@ const factory = Factory() beforeEach(async () => { await factory.create('User', { email: 'user@example.org', password: '1234' }) - await factory.create('User', { email: 'someone@example.org', password: '1234' }) + await factory.create('User', { + email: 'someone@example.org', + password: '1234' + }) headers = await login({ email: 'user@example.org', password: '1234' }) authenticatedClient = new GraphQLClient(host, { headers }) }) @@ -26,7 +29,9 @@ describe('slugify', () => { content: "Some content" ) { slug } }`) - expect(response).toEqual({ CreatePost: { slug: 'i-am-a-brand-new-post' } }) + expect(response).toEqual({ + CreatePost: { slug: 'i-am-a-brand-new-post' } + }) }) describe('if slug exists', () => { @@ -48,12 +53,15 @@ describe('slugify', () => { content: "Some content" ) { slug } }`) - expect(response).toEqual({ CreatePost: { slug: 'pre-existing-post-1' } }) + expect(response).toEqual({ + CreatePost: { slug: 'pre-existing-post-1' } + }) }) describe('but if the client specifies a slug', () => { it('rejects CreatePost', async () => { - await expect(authenticatedClient.request(`mutation { + await expect( + authenticatedClient.request(`mutation { CreatePost( title: "Pre-existing post", content: "Some content", @@ -73,28 +81,33 @@ describe('slugify', () => { }`) } it('generates a slug based on name', async () => { - await expect(action('CreateUser', 'name: "I am a user"')) - .resolves.toEqual({ CreateUser: { slug: 'i-am-a-user' } }) + await expect( + action('CreateUser', 'name: "I am a user"') + ).resolves.toEqual({ CreateUser: { slug: 'i-am-a-user' } }) }) describe('if slug exists', () => { beforeEach(async () => { - await action('CreateUser', 'name: "Pre-existing user", slug: "pre-existing-user"') + await action( + 'CreateUser', + 'name: "Pre-existing user", slug: "pre-existing-user"' + ) }) it('chooses another slug', async () => { - await expect(action( - 'CreateUser', - 'name: "pre-existing-user"' - )).resolves.toEqual({ CreateUser: { slug: 'pre-existing-user-1' } }) + await expect( + action('CreateUser', 'name: "pre-existing-user"') + ).resolves.toEqual({ CreateUser: { slug: 'pre-existing-user-1' } }) }) describe('but if the client specifies a slug', () => { it('rejects CreateUser', async () => { - await expect(action( - 'CreateUser', - 'name: "Pre-existing user", slug: "pre-existing-user"' - )).rejects.toThrow('already exists') + await expect( + action( + 'CreateUser', + 'name: "Pre-existing user", slug: "pre-existing-user"' + ) + ).rejects.toThrow('already exists') }) }) }) diff --git a/src/resolvers/follow.spec.js b/src/resolvers/follow.spec.js index 3c16560e5..081e49081 100644 --- a/src/resolvers/follow.spec.js +++ b/src/resolvers/follow.spec.js @@ -4,6 +4,7 @@ import { host, login } from '../jest/helpers' const factory = Factory() let clientUser1 +let headersUser1 const mutationFollowUser = (id) => ` mutation { @@ -27,18 +28,25 @@ beforeEach(async () => { email: 'test2@example.org', password: '1234' }) + + headersUser1 = await login({ email: 'test@example.org', password: '1234' }) + clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) }) afterEach(async () => { await factory.cleanDatabase() }) -describe('follow ', () => { - describe('(un)follow user', () => { - let headersUser1 - beforeEach(async () => { - headersUser1 = await login({ email: 'test@example.org', password: '1234' }) - clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) +describe('follow', () => { + describe('follow user', () => { + describe('unauthenticated follow', () => { + it('throws authorization error', async () => { + let client + client = new GraphQLClient(host) + await expect( + client.request(mutationFollowUser('u2')) + ).rejects.toThrow('Not Authorised') + }) }) it('I can follow another user', async () => { @@ -65,31 +73,6 @@ describe('follow ', () => { expect(User[0]).toMatchObject(expected2) }) - it('I can unfollow a user', async () => { - // follow - await clientUser1.request( - mutationFollowUser('u2') - ) - const expected = { - unfollow: true - } - // unfollow - const res = await clientUser1.request(mutationUnfollowUser('u2')) - expect(res).toMatchObject(expected) - - const { User } = await clientUser1.request(`{ - User(id: "u2") { - followedBy { id } - followedByCurrentUser - } - }`) - const expected2 = { - followedBy: [], - followedByCurrentUser: false - } - expect(User[0]).toMatchObject(expected2) - }) - it('I can`t follow myself', async () => { const res = await clientUser1.request( mutationFollowUser('u1') @@ -112,4 +95,45 @@ describe('follow ', () => { expect(User[0]).toMatchObject(expected2) }) }) + describe('unfollow user', () => { + describe('unauthenticated follow', () => { + it('throws authorization error', async () => { + // follow + await clientUser1.request( + mutationFollowUser('u2') + ) + // unfollow + let client + client = new GraphQLClient(host) + await expect( + client.request(mutationUnfollowUser('u2')) + ).rejects.toThrow('Not Authorised') + }) + }) + + 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 = { + followedBy: [], + followedByCurrentUser: false + } + expect(User[0]).toMatchObject(expected2) + }) + }) }) diff --git a/src/resolvers/shout.spec.js b/src/resolvers/shout.spec.js index 490191c7a..88866a74f 100644 --- a/src/resolvers/shout.spec.js +++ b/src/resolvers/shout.spec.js @@ -4,6 +4,7 @@ import { host, login } from '../jest/helpers' const factory = Factory() let clientUser1, clientUser2 +let headersUser1, headersUser2 const mutationShoutPost = (id) => ` mutation { @@ -27,37 +28,44 @@ beforeEach(async () => { email: 'test2@example.org', password: '1234' }) + + headersUser1 = await login({ email: 'test@example.org', password: '1234' }) + headersUser2 = await login({ email: 'test2@example.org', password: '1234' }) + clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) + clientUser2 = new GraphQLClient(host, { headers: headersUser2 }) + + await clientUser1.request(` + mutation { + CreatePost(id: "p1", title: "Post Title 1", content: "Some Post Content 1") { + id + title + } + } + `) + await clientUser2.request(` + mutation { + CreatePost(id: "p2", title: "Post Title 2", content: "Some Post Content 2") { + id + title + } + } + `) }) afterEach(async () => { await factory.cleanDatabase() }) -describe('shout ', () => { - describe('(un)shout foreign post', () => { - let headersUser1, headersUser2 - beforeEach(async () => { - headersUser1 = await login({ email: 'test@example.org', password: '1234' }) - headersUser2 = await login({ email: 'test2@example.org', password: '1234' }) - clientUser1 = new GraphQLClient(host, { headers: headersUser1 }) - clientUser2 = new GraphQLClient(host, { headers: headersUser2 }) - - await clientUser1.request(` - mutation { - CreatePost(id: "p1", title: "Post Title 1", content: "Some Post Content 1") { - id - title - } - } - `) - await clientUser2.request(` - mutation { - CreatePost(id: "p2", title: "Post Title 2", content: "Some Post Content 2") { - id - title - } - } - `) +describe('shout', () => { + describe('shout foreign post', () => { + describe('unauthenticated shout', () => { + it('throws authorization error', async () => { + let client + client = new GraphQLClient(host) + await expect( + client.request(mutationShoutPost('p1')) + ).rejects.toThrow('Not Authorised') + }) }) it('I shout a post of another user', async () => { @@ -80,29 +88,6 @@ describe('shout ', () => { expect(Post[0]).toMatchObject(expected2) }) - it('I unshout a post of another user', async () => { - // shout - await clientUser1.request( - mutationShoutPost('p2') - ) - const expected = { - unshout: true - } - // unshout - const res = await clientUser1.request(mutationUnshoutPost('p2')) - expect(res).toMatchObject(expected) - - const { Post } = await clientUser1.request(`{ - Post(id: "p2") { - shoutedByCurrentUser - } - }`) - const expected2 = { - shoutedByCurrentUser: false - } - expect(Post[0]).toMatchObject(expected2) - }) - it('I can`t shout my own post', async () => { const res = await clientUser1.request( mutationShoutPost('p1') @@ -123,4 +108,44 @@ describe('shout ', () => { expect(Post[0]).toMatchObject(expected2) }) }) + + describe('unshout foreign post', () => { + describe('unauthenticated shout', () => { + it('throws authorization error', async () => { + // shout + await clientUser1.request( + mutationShoutPost('p2') + ) + // unshout + let client + client = new GraphQLClient(host) + await expect( + client.request(mutationUnshoutPost('p2')) + ).rejects.toThrow('Not Authorised') + }) + }) + + it('I unshout a post of another user', async () => { + // shout + await clientUser1.request( + mutationShoutPost('p2') + ) + const expected = { + unshout: true + } + // unshout + const res = await clientUser1.request(mutationUnshoutPost('p2')) + expect(res).toMatchObject(expected) + + const { Post } = await clientUser1.request(`{ + Post(id: "p2") { + shoutedByCurrentUser + } + }`) + const expected2 = { + shoutedByCurrentUser: false + } + expect(Post[0]).toMatchObject(expected2) + }) + }) }) diff --git a/src/schema.graphql b/src/schema.graphql index 4e0347cc7..152301715 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -4,6 +4,18 @@ type Query { currentUser: User "Get the latest Network Statistics" statistics: Statistics! + 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) + WHERE score >= 0.2 + AND NOT user.deleted = true AND NOT user.disabled = true + AND NOT post.deleted = true AND NOT post.disabled = true + RETURN post + LIMIT $limit + """ + ) } type Mutation { "Get a JWT Token for the given Email and password" @@ -27,7 +39,6 @@ type Mutation { DELETE r RETURN COUNT(r) > 0 """) - "Follow the given Type and ID" follow(id: ID!, type: FollowTypeEnum): Boolean! @cypher(statement: """ MATCH (n {id: $id}), (u:User {id: $cypherParams.currentUserId}) diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index 2629ce8b6..e19239ece 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -1,6 +1,5 @@ import { GraphQLClient, request } from 'graphql-request' import { getDriver } from '../../bootstrap/neo4j' - import createBadge from './badges.js' import createUser from './users.js' import createOrganization from './organizations.js' @@ -23,26 +22,24 @@ const authenticatedHeaders = async ({ email, password }, host) => { } } const factories = { - 'Badge': createBadge, - 'User': createUser, - 'Organization': createOrganization, - 'Post': createPost, - 'Comment': createComment, - 'Category': createCategory, - 'Tag': createTag, - 'Report': createReport + Badge: createBadge, + User: createUser, + Organization: createOrganization, + Post: createPost, + Comment: createComment, + Category: createCategory, + Tag: createTag, + Report: createReport } export const cleanDatabase = async (options = {}) => { - const { - driver = getDriver() - } = options + const { driver = getDriver() } = options const session = driver.session() const cypher = 'MATCH (n) DETACH DELETE n' try { return await session.run(cypher) } catch (error) { - throw (error) + throw error } finally { session.close() } @@ -63,7 +60,10 @@ export default function Factory (options = {}) { factories, lastResponse: null, async authenticateAs ({ email, password }) { - const headers = await authenticatedHeaders({ email, password }, seedServerHost) + const headers = await authenticatedHeaders( + { email, password }, + seedServerHost + ) this.lastResponse = headers this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) return this