diff --git a/Dockerfile b/Dockerfile index e6dcca3..977bdd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM bbyars/mountebank:2.4.0 -ENV MB_GRAPHQL_VERSION=0.1.1 +ENV MB_GRAPHQL_VERSION=0.1.2 RUN npm install -g mb-graphql@${MB_GRAPHQL_VERSION} --production RUN mkdir /mb-graphql diff --git a/README.md b/README.md index 4d240be..1e1c8c5 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,10 @@ Create the imposter via mountebank (assuming it's running on `localhost:2525`): curl -i -X POST -H 'Content-Type: application/json' http://localhost:2525/imposters --data @imposter.json ``` +You can now access the GraphQL playground for the imposter at `http://localhost:4000`: + +![GraphQL Playground](./playground.png) + ### Request ```graphql @@ -114,16 +118,16 @@ Note: The value for `myQuery.alpha` has been randomly generated as it was omitte For further information about mountebank imposters, stubs and related concepts please refer to the [mountbank mental model](https://www.mbtest.org/docs/mentalModel). -| Parameter | Description | Required? | Default | -|-------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------|----------------------------------------------------------------------------------------------| -| `protocol` | Must be set to `graphql` | Yes | N/A | -| `defaultResponse` | Important: Do not set as it will interfere with GraphQL resolution. | No. Do not set. | Default behaviour for the imposter is to return random data according to the defined schema. | -| `port` | The port to run the imposter on. | No | A randomly assigned port. mountebank will return the actual value in the `POST` response. | -| `name` | Included in the logs, useful when multiple imposters are set up. | No | An empty string. | -| `schema` | A string presenting a valid GraphQL schema definition. | No, if `schemaEndpoint` is set | N/A | -| `schemaEndpoint` | The endpoint of an existing GraphQL API which exposes the GraphQL introspection query. | No, if `schema` is set | N/A | -| `schemaEndpointHeaders` | An object representing headers to passed on to the GraphQL API defined in `schemaEndpoint` e.g. `Authorization` header. | No | An empty object. | -| `stubs` | The list of stubs responsible for matching a GraphQL request and returning a response. See further details below. | No | An empty array. | +| Parameter | Description | Required? | Default | +|-------------------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------|----------------------------------------------------------------------------------------------| +| `protocol` | Must be set to `graphql` | Yes | N/A | +| `defaultResponse` | Important: Do not set as it will interfere with GraphQL resolution. | No. Do not set. | Default behaviour for the imposter is to return random data according to the defined schema. | +| `port` | The port to run the imposter on. | No | A randomly assigned port. mountebank will return the actual value in the `POST` response. | +| `name` | Included in the logs, useful when multiple imposters are set up. | No | An empty string. | +| `schema` | A string presenting a valid GraphQL schema definition. | No, if `schemaEndpoint` is set | N/A | +| `schemaEndpoint` | URL of a GraphQL schema file or the endpoint of an existing GraphQL API which exposes the GraphQL introspection query. | No, if `schema` is set | N/A | +| `schemaEndpointHeaders` | An object representing headers to passed to the schema endpoint defined in `schemaEndpoint` e.g. `Authorization` header. | No | An empty object. | +| `stubs` | The list of stubs responsible for matching a GraphQL request and returning a response. See further details below. | No | An empty array. | ## GraphQL Requests diff --git a/package-lock.json b/package-lock.json index 3ec6ee0..051427b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mb-graphql", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5209f77..0f874d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mb-graphql", - "version": "0.1.1", + "version": "0.1.2", "description": "mountebank test doubles for GraphQL", "author": "bashj79", "license": "MIT", diff --git a/playground.png b/playground.png new file mode 100644 index 0000000..f625d8a Binary files /dev/null and b/playground.png differ diff --git a/src/createImposterSchema.js b/src/createImposterSchema.js deleted file mode 100644 index 19e8df9..0000000 --- a/src/createImposterSchema.js +++ /dev/null @@ -1,20 +0,0 @@ -import { loadSchema } from '@graphql-tools/load'; -import { UrlLoader } from '@graphql-tools/url-loader'; -import { applyMiddleware } from 'graphql-middleware'; - -import mountebankHandler from './mountebankHandler'; - -export default async ({ - schema, - schemaEndpoint, - schemaEndpointHeaders = {}, -}) => { - const imposterSchema = !schemaEndpoint - ? await loadSchema(schema) - : await loadSchema(schemaEndpoint, { - loaders: [new UrlLoader()], - headers: schemaEndpointHeaders, - }); - - return applyMiddleware(imposterSchema, mountebankHandler); -}; diff --git a/src/getOperationTypeAndPathKey.js b/src/handle-operation/getOperationTypeAndPathKey.js similarity index 100% rename from src/getOperationTypeAndPathKey.js rename to src/handle-operation/getOperationTypeAndPathKey.js diff --git a/src/getOperationTypeAndPathKey.test.js b/src/handle-operation/getOperationTypeAndPathKey.test.js similarity index 91% rename from src/getOperationTypeAndPathKey.test.js rename to src/handle-operation/getOperationTypeAndPathKey.test.js index 085a92d..15471ff 100644 --- a/src/getOperationTypeAndPathKey.test.js +++ b/src/handle-operation/getOperationTypeAndPathKey.test.js @@ -12,7 +12,8 @@ describe('getOperationTypeAndPathKey', () => { }; const result = getOperationTypeAndPathKey(path); - expect(result).toEqual(expected); + expect(result) + .toEqual(expected); }); it('should get operation type and path key with path having single previous path', () => { @@ -30,7 +31,8 @@ describe('getOperationTypeAndPathKey', () => { }; const result = getOperationTypeAndPathKey(path); - expect(result).toEqual(expected); + expect(result) + .toEqual(expected); }); it('should get operation type and path key with path having multiple previous paths', () => { @@ -52,7 +54,8 @@ describe('getOperationTypeAndPathKey', () => { }; const result = getOperationTypeAndPathKey(path); - expect(result).toEqual(expected); + expect(result) + .toEqual(expected); }); it('should get operation type and path key with path having previous path with no type name', () => { @@ -73,6 +76,7 @@ describe('getOperationTypeAndPathKey', () => { }; const result = getOperationTypeAndPathKey(path); - expect(result).toEqual(expected); + expect(result) + .toEqual(expected); }); }); diff --git a/src/getPathElements.js b/src/handle-operation/getPathElements.js similarity index 100% rename from src/getPathElements.js rename to src/handle-operation/getPathElements.js diff --git a/src/getPathElements.test.js b/src/handle-operation/getPathElements.test.js similarity index 91% rename from src/getPathElements.test.js rename to src/handle-operation/getPathElements.test.js index 236f936..388d633 100644 --- a/src/getPathElements.test.js +++ b/src/handle-operation/getPathElements.test.js @@ -14,7 +14,8 @@ describe('getPathElements', () => { ]; const result = getPathElements(path); - expect(result).toEqual(expected); + expect(result) + .toEqual(expected); }); it('should get path elements with path having single previous path', () => { @@ -38,7 +39,8 @@ describe('getPathElements', () => { ]; const result = getPathElements(path); - expect(result).toEqual(expected); + expect(result) + .toEqual(expected); }); it('should get path elements with path having multiple previous path with no type', () => { @@ -68,6 +70,7 @@ describe('getPathElements', () => { ]; const result = getPathElements(path); - expect(result).toEqual(expected); + expect(result) + .toEqual(expected); }); }); diff --git a/src/mountebankHandler.js b/src/handle-operation/mountebankHandler.js similarity index 79% rename from src/mountebankHandler.js rename to src/handle-operation/mountebankHandler.js index 104e77e..c385f39 100644 --- a/src/mountebankHandler.js +++ b/src/handle-operation/mountebankHandler.js @@ -1,5 +1,5 @@ -import invokeMountebankCallback from './invokeMountebankCallback'; -import isEmptyObject from './isEmptyObject'; +import invokeMountebankCallback from '../mountebank-adapter/invokeMountebankCallback'; +import isEmptyObject from '../utils/isEmptyObject'; import getOperationTypeAndPathKey from './getOperationTypeAndPathKey'; export default async (resolve, root, args, context, info) => { diff --git a/src/createContext.js b/src/imposter-schema-creator/createContext.js similarity index 100% rename from src/createContext.js rename to src/imposter-schema-creator/createContext.js diff --git a/src/imposter-schema-creator/createImposterSchema.js b/src/imposter-schema-creator/createImposterSchema.js new file mode 100644 index 0000000..3971728 --- /dev/null +++ b/src/imposter-schema-creator/createImposterSchema.js @@ -0,0 +1,18 @@ +import { applyMiddleware } from 'graphql-middleware'; + +import mountebankHandler from '../handle-operation/mountebankHandler'; +import loadSchema from './loadSchema'; + +export default async ({ + schema, + schemaEndpoint, + schemaEndpointHeaders = {}, +}) => { + const imposterSchema = await loadSchema({ + schema, + schemaEndpoint, + schemaEndpointHeaders, + }); + + return applyMiddleware(imposterSchema, mountebankHandler); +}; diff --git a/src/imposter-schema-creator/loadSchema.js b/src/imposter-schema-creator/loadSchema.js new file mode 100644 index 0000000..bdd2e4f --- /dev/null +++ b/src/imposter-schema-creator/loadSchema.js @@ -0,0 +1,22 @@ +import { loadSchema } from '@graphql-tools/load'; +import { UrlLoader } from '@graphql-tools/url-loader'; + +export default async ({ + schema, + schemaEndpoint, + schemaEndpointHeaders, +}) => { + const schemaToLoad = schema || schemaEndpoint; + const schemaOptions = schema + ? undefined + : { + loaders: [new UrlLoader()], + headers: schemaEndpointHeaders, + }; + try { + const loadedSchema = await loadSchema(schemaToLoad, schemaOptions); + return loadedSchema; + } catch (error) { + throw new Error(`Could not load schema: ${schemaToLoad}\n${error.message}`); + } +}; diff --git a/src/startImposter.js b/src/imposter-schema-creator/startImposter.js similarity index 81% rename from src/startImposter.js rename to src/imposter-schema-creator/startImposter.js index 4c1ec9b..2635b22 100644 --- a/src/startImposter.js +++ b/src/imposter-schema-creator/startImposter.js @@ -1,8 +1,9 @@ import { ApolloServer } from 'apollo-server'; import createImposterSchema from './createImposterSchema'; -import logger from './logger'; +import logger from '../utils/logger'; import createContext from './createContext'; +import randomString from '../mock-generator/randomString'; export default async ({ schema, @@ -17,7 +18,7 @@ export default async ({ }); const server = new ApolloServer({ schema: imposterSchema, - mocks: true, + mocks: { String: () => randomString() }, mockEntireSchema: false, context: createContext, debug: false, diff --git a/src/index.js b/src/index.js index 63eba64..2be852e 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,9 @@ import 'regenerator-runtime/runtime'; import 'core-js/stable'; -import startImposter from './startImposter'; -import config from './config'; -import logger from './logger'; +import startImposter from './imposter-schema-creator/startImposter'; +import logger from './utils/logger'; +import config from './utils/config'; startImposter({ schema: config.schema, @@ -15,8 +15,9 @@ startImposter({ }) .catch((error) => { logger({ + level: 'error', message: 'Error starting imposter', - config, error, }); + process.exit(1); }); diff --git a/src/logger.js b/src/logger.js deleted file mode 100644 index b2755f5..0000000 --- a/src/logger.js +++ /dev/null @@ -1,11 +0,0 @@ -export default ({ - level = 'info', - message, - ...data -}) => { - const formattedMessage = data - ? `${level} ${message}\n${JSON.stringify(data, null, 2)}` - : `${level} ${message}`; - // eslint-disable-next-line no-console - console.log(formattedMessage); -}; diff --git a/src/mock-generator/randomInt.js b/src/mock-generator/randomInt.js new file mode 100644 index 0000000..00aafd3 --- /dev/null +++ b/src/mock-generator/randomInt.js @@ -0,0 +1 @@ +export default (max) => Math.floor(Math.random() * max); diff --git a/src/mock-generator/randomString.js b/src/mock-generator/randomString.js new file mode 100644 index 0000000..cdf042d --- /dev/null +++ b/src/mock-generator/randomString.js @@ -0,0 +1,12 @@ +import randomInt from './randomInt'; +import toSentenceCase from './toSentenceCase'; + +const words = 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' + .split(' '); + +export default () => { + const start = randomInt(words.length); + const end = randomInt(words.length - start) + start + 1; + return toSentenceCase(words.slice(start, end) + .join(' ')); +}; diff --git a/src/mock-generator/toSentenceCase.js b/src/mock-generator/toSentenceCase.js new file mode 100644 index 0000000..7a13628 --- /dev/null +++ b/src/mock-generator/toSentenceCase.js @@ -0,0 +1 @@ +export default (input) => input[0].toUpperCase() + input.substr(1); diff --git a/src/createMountebankCallbackBody.js b/src/mountebank-adapter/createMountebankCallbackBody.js similarity index 100% rename from src/createMountebankCallbackBody.js rename to src/mountebank-adapter/createMountebankCallbackBody.js diff --git a/src/invokeMountebankCallback.js b/src/mountebank-adapter/invokeMountebankCallback.js similarity index 94% rename from src/invokeMountebankCallback.js rename to src/mountebank-adapter/invokeMountebankCallback.js index 6a968ae..c7ea7eb 100644 --- a/src/invokeMountebankCallback.js +++ b/src/mountebank-adapter/invokeMountebankCallback.js @@ -1,7 +1,7 @@ import fetch from 'node-fetch'; import createMountebankCallbackBody from './createMountebankCallbackBody'; -import config from './config'; +import config from '../utils/config'; export default async ({ operationType, diff --git a/src/config.js b/src/utils/config.js similarity index 100% rename from src/config.js rename to src/utils/config.js diff --git a/src/isEmptyObject.js b/src/utils/isEmptyObject.js similarity index 100% rename from src/isEmptyObject.js rename to src/utils/isEmptyObject.js diff --git a/src/isEmptyObject.test.js b/src/utils/isEmptyObject.test.js similarity index 75% rename from src/isEmptyObject.test.js rename to src/utils/isEmptyObject.test.js index d6ef682..4ab6ce9 100644 --- a/src/isEmptyObject.test.js +++ b/src/utils/isEmptyObject.test.js @@ -4,42 +4,49 @@ describe('isEmptyObject', () => { it('should be true if object has no properties', () => { const result = isEmptyObject({}); - expect(result).toBeTruthy(); + expect(result) + .toBeTruthy(); }); it('should be false if object has properties undefined', () => { const result = isEmptyObject({ abc: 123 }); - expect(result).toBeFalsy(); + expect(result) + .toBeFalsy(); }); it('should be false if object is undefined', () => { const result = isEmptyObject(undefined); - expect(result).toBeFalsy(); + expect(result) + .toBeFalsy(); }); it('should be false if object is null', () => { const result = isEmptyObject(null); - expect(result).toBeFalsy(); + expect(result) + .toBeFalsy(); }); it('should be false if object is number', () => { const result = isEmptyObject(123); - expect(result).toBeFalsy(); + expect(result) + .toBeFalsy(); }); it('should be false if object is string', () => { const result = isEmptyObject('abc'); - expect(result).toBeFalsy(); + expect(result) + .toBeFalsy(); }); it('should be false if object is array', () => { const result = isEmptyObject([123, 'abc']); - expect(result).toBeFalsy(); + expect(result) + .toBeFalsy(); }); }); diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..cb509d1 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,15 @@ +/* eslint-disable no-console */ +export default ({ + level = 'info', + message, + ...data +}) => { + const formattedMessage = `${level} ${message}`; + if (level === 'error') { + console.error(formattedMessage, data); + } else if (level === 'warn') { + console.warn(formattedMessage, data); + } else { + console.log(formattedMessage, data); + } +};