diff --git a/src/bootstrap/neo4j.js b/src/bootstrap/neo4j.js index 766c12065..935449a0a 100644 --- a/src/bootstrap/neo4j.js +++ b/src/bootstrap/neo4j.js @@ -1,20 +1,18 @@ import { v1 as neo4j } from 'neo4j-driver' +import dotenv from 'dotenv' + +dotenv.config() let driver -export default function () { - return { - getDriver () { - if (!driver) { - driver = neo4j.driver( - process.env.NEO4J_URI || 'bolt://localhost:7687', - neo4j.auth.basic( - process.env.NEO4J_USER || 'neo4j', - process.env.NEO4J_PASSWORD || 'neo4j' - ) - ) - } - return driver - } +export function getDriver (options = {}) { + const { + uri = process.env.NEO4J_URI || 'bolt://localhost:7687', + username = process.env.NEO4J_USERNAME || 'neo4j', + password = process.env.NEO4J_PASSWORD || 'neo4j' + } = options + if (!driver) { + driver = neo4j.driver(uri, neo4j.auth.basic(username, password)) } + return driver } diff --git a/src/graphql-schema.spec.js b/src/graphql-schema.spec.js index 69073444c..cedecac26 100644 --- a/src/graphql-schema.spec.js +++ b/src/graphql-schema.spec.js @@ -1,7 +1,9 @@ -import { request } from 'graphql-request' -import { create, cleanDatabase } from './seed/factories' +import { GraphQLClient, request } from 'graphql-request' +import Factory from './seed/factories' import jwt from 'jsonwebtoken' -import { host } from './jest/helpers' +import { host, login } from './jest/helpers' + +const factory = Factory() describe('login', () => { const mutation = (params) => { @@ -16,14 +18,14 @@ describe('login', () => { describe('given an existing user', () => { beforeEach(async () => { - await create('user', { + await factory.create('user', { email: 'test@example.org', password: '1234' }) }) afterEach(async () => { - await cleanDatabase() + await factory.cleanDatabase() }) describe('asking for a `token`', () => { @@ -60,3 +62,66 @@ describe('login', () => { }) }) }) + +describe('report', () => { + beforeEach(async () => { + await factory.create('user', { + email: 'test@example.org', + password: '1234' + }) + await factory.create('user', { + id: 'u2', + name: 'abusive-user', + role: 'user', + email: 'abusive-user@example.org' + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('unauthenticated', () => { + let client + it('throws authorization error', async () => { + client = new GraphQLClient(host) + await expect( + client.request(`mutation { + report( + description: "I don't like this user", + resource: { + id: "u2", + type: user + } + ) { id, createdAt } + }`) + ).rejects.toThrow('Not Authorised') + }) + + describe('authenticated', () => { + let headers + let response + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + response = await client.request(`mutation { + report( + description: "I don't like this user", + resource: { + id: "u2", + type: user + } + ) { id, createdAt } + }`, + { headers } + ) + }) + it('creates a report', () => { + let { id, createdAt } = response.report + expect(response).toEqual({ + report: { id, createdAt } + }) + }) + }) + }) +}) diff --git a/src/jest/helpers.js b/src/jest/helpers.js index 01a26e9d3..ff6a535e2 100644 --- a/src/jest/helpers.js +++ b/src/jest/helpers.js @@ -1,8 +1,10 @@ import { request } from 'graphql-request' +// this is the to-be-tested server host +// not to be confused with the seeder host export const host = 'http://127.0.0.1:4123' -export async function authenticatedHeaders ({ email, password }) { +export async function login ({ email, password }) { const mutation = ` mutation { login(email:"${email}", password:"${password}"){ diff --git a/src/middleware/permissionsMiddleware.spec.js b/src/middleware/permissionsMiddleware.spec.js index cf86d11c9..9b115eae3 100644 --- a/src/middleware/permissionsMiddleware.spec.js +++ b/src/middleware/permissionsMiddleware.spec.js @@ -1,5 +1,5 @@ import { create, cleanDatabase } from '../seed/factories' -import { host, authenticatedHeaders } from '../jest/helpers' +import { host, login } from '../jest/helpers' import { GraphQLClient } from 'graphql-request' describe('authorization', () => { @@ -45,7 +45,7 @@ describe('authorization', () => { describe('as owner', () => { it('exposes the owner\'s email address', async () => { - headers = await authenticatedHeaders({ + headers = await login({ email: 'owner@example.org', password: 'iamtheowner' }) @@ -55,7 +55,7 @@ describe('authorization', () => { describe('as someone else', () => { it('does not expose the owner\'s email address', async () => { - headers = await authenticatedHeaders({ + headers = await login({ email: 'someone@example.org', password: 'else' }) diff --git a/src/seed/factories/badges.js b/src/seed/factories/badges.js new file mode 100644 index 000000000..b34442521 --- /dev/null +++ b/src/seed/factories/badges.js @@ -0,0 +1,23 @@ +import uuid from 'uuid/v4' + +export default function (params) { + const { + id = uuid(), + key, + type = 'crowdfunding', + status = 'permanent', + icon + } = params + + return ` + mutation { + CreateBadge( + id: "${id}", + key: "${key}", + type: ${type}, + status: ${status}, + icon: "${icon}" + ) { id } + } + ` +} diff --git a/src/seed/factories/categories.js b/src/seed/factories/categories.js new file mode 100644 index 000000000..a4b448f4b --- /dev/null +++ b/src/seed/factories/categories.js @@ -0,0 +1,21 @@ +import uuid from 'uuid/v4' + +export default function (params) { + const { + id = uuid(), + name, + slug, + icon + } = params + + return ` + mutation { + CreateCategory( + id: "${id}", + name: "${name}", + slug: "${slug}", + icon: "${icon}" + ) { id, name } + } + ` +} diff --git a/src/seed/factories/comments.js b/src/seed/factories/comments.js new file mode 100644 index 000000000..acf493f6d --- /dev/null +++ b/src/seed/factories/comments.js @@ -0,0 +1,37 @@ +import faker from 'faker' +import uuid from 'uuid/v4' + +export default function (params) { + const { + id = uuid(), + content = [ + faker.lorem.sentence(), + faker.lorem.sentence() + ].join('. '), + disabled = false, + deleted = false + } = params + + return ` + mutation { + CreateComment( + id: "${id}", + content: "${content}", + disabled: ${disabled}, + deleted: ${deleted} + ) { id } + } + ` +} + +export function relate (type, params) { + const { from, to } = params + return ` + mutation { + ${from}_${type}_${to}: AddComment${type}( + from: { id: "${from}" }, + to: { id: "${to}" } + ) { from { id } } + } + ` +} diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index e62e98869..a107fc6b7 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -1,50 +1,103 @@ -import ApolloClient from 'apollo-client' -import gql from 'graphql-tag' -import dotenv from 'dotenv' -import { HttpLink } from 'apollo-link-http' -import { InMemoryCache } from 'apollo-cache-inmemory' -import neo4j from '../../bootstrap/neo4j' -import fetch from 'node-fetch' +import { GraphQLClient, request } from 'graphql-request' +import { getDriver } from '../../bootstrap/neo4j' -dotenv.config() +export const seedServerHost = 'http://127.0.0.1:4001' -if (process.env.NODE_ENV === 'production') { - throw new Error('YOU CAN`T RUN FACTORIES IN PRODUCTION MODE') -} - -const client = new ApolloClient({ - link: new HttpLink({ uri: 'http://localhost:4001', fetch }), - cache: new InMemoryCache() -}) - -const driver = neo4j().getDriver() - -const builders = { - 'user': require('./users.js').default -} - -const buildMutation = (model, parameters) => { - return builders[model](parameters) -} - -const create = (model, parameters) => { - return client.mutate({ mutation: gql(buildMutation(model, parameters)) }) -} - -const cleanDatabase = async () => { - const session = driver.session() - const cypher = 'MATCH (n) DETACH DELETE n' - try { - const result = await session.run(cypher) - session.close() - return result - } catch (error) { - console.log(error) +const authenticatedHeaders = async ({ email, password }, host) => { + const mutation = ` + mutation { + login(email:"${email}", password:"${password}"){ + token + } + }` + const response = await request(host, mutation) + return { + authorization: `Bearer ${response.login.token}` } } -export { - create, - buildMutation, - cleanDatabase +const factories = { + 'badge': require('./badges.js').default, + 'user': require('./users.js').default, + 'organization': require('./organizations.js').default, + 'post': require('./posts.js').default, + 'comment': require('./comments.js').default, + 'category': require('./categories.js').default, + 'tag': require('./tags.js').default, + 'report': require('./reports.js').default +} + +const relationFactories = { + 'user': require('./users.js').relate, + 'organization': require('./organizations.js').relate, + 'post': require('./posts.js').relate, + 'comment': require('./comments.js').relate +} + +export const create = (model, parameters, options) => { + const graphQLClient = new GraphQLClient(seedServerHost, options) + const mutation = factories[model](parameters) + return graphQLClient.request(mutation) +} + +export const relate = (model, type, parameters, options) => { + const graphQLClient = new GraphQLClient(seedServerHost, options) + const mutation = relationFactories[model](type, parameters) + return graphQLClient.request(mutation) +} + +export const cleanDatabase = async (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) + } finally { + session.close() + } +} + +export default function Factory (options = {}) { + const { + neo4jDriver = getDriver(), + seedServerHost = 'http://127.0.0.1:4001' + } = options + + const graphQLClient = new GraphQLClient(seedServerHost) + + const result = { + neo4jDriver, + seedServerHost, + graphQLClient, + lastResponse: null, + async authenticateAs ({ email, password }) { + const headers = await authenticatedHeaders({ email, password }, seedServerHost) + this.lastResponse = headers + this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) + return this + }, + async create (node, properties) { + const mutation = factories[node](properties) + this.lastResponse = await this.graphQLClient.request(mutation) + return this + }, + async relate (node, relationship, properties) { + const mutation = relationFactories[node](relationship, properties) + this.lastResponse = await this.graphQLClient.request(mutation) + return this + }, + async cleanDatabase () { + this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) + return this + } + } + result.authenticateAs.bind(result) + result.create.bind(result) + result.relate.bind(result) + result.cleanDatabase.bind(result) + return result } diff --git a/src/seed/factories/organizations.js b/src/seed/factories/organizations.js new file mode 100644 index 000000000..0ab73beb8 --- /dev/null +++ b/src/seed/factories/organizations.js @@ -0,0 +1,36 @@ +import faker from 'faker' +import uuid from 'uuid/v4' + +export default function create (params) { + const { + id = uuid(), + name = faker.comany.companyName(), + description = faker.company.catchPhrase(), + disabled = false, + deleted = false + } = params + + return ` + mutation { + CreateOrganization( + id: "${id}", + name: "${name}", + description: "${description}", + disabled: ${disabled}, + deleted: ${deleted} + ) { name } + } + ` +} + +export function relate (type, params) { + const { from, to } = params + return ` + mutation { + ${from}_${type}_${to}: AddOrganization${type}( + from: { id: "${from}" }, + to: { id: "${to}" } + ) { from { id } } + } + ` +} diff --git a/src/seed/factories/posts.js b/src/seed/factories/posts.js new file mode 100644 index 000000000..80f5e289d --- /dev/null +++ b/src/seed/factories/posts.js @@ -0,0 +1,46 @@ +import faker from 'faker' +import uuid from 'uuid/v4' + +export default function (params) { + const { + id = uuid(), + title = faker.lorem.sentence(), + content = [ + faker.lorem.sentence(), + faker.lorem.sentence(), + faker.lorem.sentence(), + faker.lorem.sentence(), + faker.lorem.sentence() + ].join('. '), + image = faker.image.image(), + visibility = 'public', + disabled = false, + deleted = false + } = params + + return ` + mutation { + CreatePost( + id: "${id}", + title: "${title}", + content: "${content}", + image: "${image}", + visibility: ${visibility}, + disabled: ${disabled}, + deleted: ${deleted} + ) { title, content } + } + ` +} + +export function relate (type, params) { + const { from, to } = params + return ` + mutation { + ${from}_${type}_${to}: AddPost${type}( + from: { id: "${from}" }, + to: { id: "${to}" } + ) { from { id } } + } + ` +} diff --git a/src/seed/factories/reports.js b/src/seed/factories/reports.js new file mode 100644 index 000000000..4dcd479f1 --- /dev/null +++ b/src/seed/factories/reports.js @@ -0,0 +1,23 @@ +import faker from 'faker' + +export default function create (params) { + const { + description = faker.lorem.sentence(), + resource: { id: resourceId, type } + } = params + + return ` + mutation { + report( + description: "${description}", + resource: { + id: "${resourceId}", + type: ${type} + } + ) { + id, + createdAt + } + } + ` +} diff --git a/src/seed/factories/tags.js b/src/seed/factories/tags.js new file mode 100644 index 000000000..c603c5629 --- /dev/null +++ b/src/seed/factories/tags.js @@ -0,0 +1,17 @@ +import uuid from 'uuid/v4' + +export default function (params) { + const { + id = uuid(), + name + } = params + + return ` + mutation { + CreateTag( + id: "${id}", + name: "${name}", + ) { name } + } + ` +} diff --git a/src/seed/factories/users.js b/src/seed/factories/users.js index 452059a73..b3a6e83c1 100644 --- a/src/seed/factories/users.js +++ b/src/seed/factories/users.js @@ -1,25 +1,30 @@ import faker from 'faker' +import uuid from 'uuid/v4' -export default function (params) { +export default function create (params) { const { + id = uuid(), name = faker.name.findName(), email = faker.internet.email(), password = '1234', - avatar = faker.internet.avatar() + role = 'user', + avatar = faker.internet.avatar(), + disabled = false, + deleted = false } = params return ` mutation { - u1: CreateUser( - id: "u1", + CreateUser( + id: "${id}", name: "${name}", password: "${password}", email: "${email}", avatar: "${avatar}", - role: admin, - disabled: false, - deleted: false) { - id + role: ${role}, + disabled: ${disabled}, + deleted: ${deleted} + ) { name email avatar @@ -28,3 +33,15 @@ export default function (params) { } ` } + +export function relate (type, params) { + const { from, to } = params + return ` + mutation { + ${from}_${type}_${to}: AddUser${type}( + from: { id: "${from}" }, + to: { id: "${to}" } + ) { from { id } } + } + ` +} diff --git a/src/seed/reset-db.js b/src/seed/reset-db.js index 7d7c4f3f9..4075489f9 100644 --- a/src/seed/reset-db.js +++ b/src/seed/reset-db.js @@ -1,30 +1,19 @@ -import { query } from '../graphql-schema' +import { cleanDatabase } from './factories' import dotenv from 'dotenv' -import neo4j from '../bootstrap/neo4j' dotenv.config() if (process.env.NODE_ENV === 'production') { - throw new Error('YOU CAN`T UNSEED IN PRODUCTION MODE') + throw new Error(`YOU CAN'T CLEAN THE DATABASE WITH NODE_ENV=${process.env.NODE_ENV}`) } -const driver = neo4j().getDriver() -const session = driver.session() - -const deleteAll = ` -MATCH (n) -OPTIONAL MATCH (n)-[r]-() -DELETE n,r -` -query(deleteAll, session).then(() => { - /* eslint-disable-next-line no-console */ - console.log('Successfully deleted all nodes and relations!') -}).catch((err) => { - /* eslint-disable-next-line no-console */ - console.log(`Error occurred deleting the nodes and relations (reset the db)\n\n${err}`) -}).finally(() => { - if (session) { - session.close() +(async function () { + try { + await cleanDatabase() + console.log('Successfully deleted all nodes and relations!') + process.exit(0) + } catch (err) { + console.log(`Error occurred deleting the nodes and relations (reset the db)\n\n${err}`) + process.exit(1) } - process.exit(0) -}) +})() diff --git a/src/server.js b/src/server.js index 1d85cbd00..5867e6952 100644 --- a/src/server.js +++ b/src/server.js @@ -7,7 +7,7 @@ import mocks from './mocks' import middleware from './middleware' import applyDirectives from './bootstrap/directives' import applyScalars from './bootstrap/scalars' -import neo4j from './bootstrap/neo4j' +import { getDriver } from './bootstrap/neo4j' import passport from 'passport' import jwtStrategy from './jwt/strategy' @@ -22,7 +22,7 @@ requiredEnvVars.forEach(env => { } }) -const driver = neo4j().getDriver() +const driver = getDriver() const debug = process.env.NODE_ENV !== 'production' && process.env.DEBUG === 'true' let schema = makeAugmentedSchema({