From 641e0e7f947d9b5b6d538c9988a77094e6266e94 Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Sun, 22 Apr 2018 16:46:11 +1000 Subject: [PATCH] feat(graphql): add basic GraphQL wrapper function --- examples/e2e/test/publish.js | 2 +- examples/graphql/consumer.js | 35 ------ examples/graphql/consumer.spec.js | 60 ----------- examples/graphql/consumer.spec.ts | 61 +++++++++++ examples/graphql/consumer.ts | 30 ++++++ examples/graphql/foo.js | 1 + examples/graphql/package.json | 17 ++- examples/graphql/provider.js | 29 ----- examples/graphql/provider.spec.js | 32 ------ examples/graphql/provider.spec.ts | 37 +++++++ examples/graphql/provider.ts | 26 +++++ examples/graphql/publish.js | 23 ++++ examples/messages/publish.js | 2 +- examples/serverless/publish.js | 2 +- package.json | 8 +- src/dsl/interaction.spec.ts | 23 ++-- src/dsl/interaction.ts | 9 +- src/dsl/utils.ts | 172 ++++++++++++++++++------------ 18 files changed, 322 insertions(+), 247 deletions(-) delete mode 100644 examples/graphql/consumer.js delete mode 100644 examples/graphql/consumer.spec.js create mode 100644 examples/graphql/consumer.spec.ts create mode 100644 examples/graphql/consumer.ts create mode 100644 examples/graphql/foo.js delete mode 100644 examples/graphql/provider.js delete mode 100644 examples/graphql/provider.spec.js create mode 100644 examples/graphql/provider.spec.ts create mode 100644 examples/graphql/provider.ts create mode 100644 examples/graphql/publish.js diff --git a/examples/e2e/test/publish.js b/examples/e2e/test/publish.js index 51b39f2d6..55c5edd24 100644 --- a/examples/e2e/test/publish.js +++ b/examples/e2e/test/publish.js @@ -6,7 +6,7 @@ const opts = { pactBrokerUsername: 'dXfltyFMgNOFZAxr8io9wJ37iUpY42M', pactBrokerPassword: 'O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1', tags: ['prod', 'test'], - consumerVersion: '1.0.1' + consumerVersion: '1.0.' + ((process.env.TRAVIS_BUILD_NUMBER) ? process.env.TRAVIS_BUILD_NUMBER : Math.floor(new Date() / 1000)) } pact.publishPacts(opts) diff --git a/examples/graphql/consumer.js b/examples/graphql/consumer.js deleted file mode 100644 index 2a456b558..000000000 --- a/examples/graphql/consumer.js +++ /dev/null @@ -1,35 +0,0 @@ -const { ApolloClient, HttpLink } = require('apollo-boost') -const { InMemoryCache } = require('apollo-cache-inmemory') -const gql = require('graphql-tag') -const fetch = require('node-fetch') -const { createHttpLink } = require('apollo-link-http') -const link = createHttpLink({ - uri: 'http://localhost:4000/graphql', - fetch: fetch, - headers: { - "foo": "bar", - } -}) - -const client = new ApolloClient({ - link, - cache: new InMemoryCache() -}) - -const query = () => - client - .query({ - variables: { - "foo": "bar", - }, - query: gql ` - { - hello - } - ` - }) - .then(result => result.data) - -module.exports = { - query -} diff --git a/examples/graphql/consumer.spec.js b/examples/graphql/consumer.spec.js deleted file mode 100644 index 989880faf..000000000 --- a/examples/graphql/consumer.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -// POC that graphQL endpoints can be tested! -const { like } = require('../../dist/dsl/matchers') -const chai = require('chai') -const expect = chai.expect -const chaiAsPromised = require('chai-as-promised') -const path = require('path') -const { Pact, graphql } = require('../../dist/pact') -const { query } = require('./consumer.js'); - -chai.use(chaiAsPromised) - -describe('GraphQL example', () => { - const provider = new Pact({ - port: 4000, - log: path.resolve(process.cwd(), 'logs', 'mockserver-integration.log'), - dir: path.resolve(process.cwd(), 'pacts'), - consumer: 'GraphQLConsumer', - provider: 'GraphQLConsumerProvider', - }) - - before(() => provider.setup()) - after(() => provider.finalize()) - - describe('query hello on /graphql', () => { - before(() => { - // TODO: make a builder interface - const graphqlQuery = graphql({ - description: "a hello request", - operation: null, - variables: { - "foo": "bar", - }, - // TODO: make this whitespace resilient - // special DSL? - query: `{\n hello\n}\n`, - requestOptions: { - path: '/graphql', - }, - }, { - status: 200, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - }, - body: { - "data": { - "hello": like("Hello world!") - } - } - }) - return provider.addInteraction(graphqlQuery) - }) - - it('returns the correct response', (done) => { - expect(query()).to.eventually.deep.eq({ hello: 'Hello world!' }).notify(done) - }) - - // verify with Pact, and reset expectations - afterEach(() => provider.verify()) - }) -}) diff --git a/examples/graphql/consumer.spec.ts b/examples/graphql/consumer.spec.ts new file mode 100644 index 000000000..4899a610c --- /dev/null +++ b/examples/graphql/consumer.spec.ts @@ -0,0 +1,61 @@ +// POC that graphQL endpoints can be tested! +/* tslint:disable:no-unused-expression object-literal-sort-keys max-classes-per-file no-empty */ + +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +import * as sinon from "sinon"; +import { like, term } from "../../src/dsl/matchers"; +import { query } from "./consumer"; +import { Pact, GraphQLInteraction } from "../../src/pact"; + +const path = require("path"); +const expect = chai.expect; + +chai.use(chaiAsPromised); + +describe("GraphQL example", () => { + const provider = new Pact({ + port: 4000, + log: path.resolve(process.cwd(), "logs", "mockserver-integration.log"), + dir: path.resolve(process.cwd(), "pacts"), + consumer: "GraphQLConsumer", + provider: "GraphQLProvider", + }); + + before(() => provider.setup()); + after(() => provider.finalize()); + + describe("query hello on /graphql", () => { + before(() => { + const graphqlQuery = new GraphQLInteraction() + .uponReceiving("a hello request") + .withQuery(`{ hello }`) + .withRequest({ + path: "/graphql", + method: "POST", + }) + .withVariables({ + foo: "bar", + }) + .willRespondWith({ + status: 200, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + body: { + data: { + hello: like("Hello world!"), + }, + }, + }); + return provider.addInteraction(graphqlQuery); + }); + + it("returns the correct response", (done) => { + expect(query()).to.eventually.deep.eq({ hello: "Hello world!" }).notify(done); + }); + + // verify with Pact, and reset expectations + afterEach(() => provider.verify()); + }); +}); diff --git a/examples/graphql/consumer.ts b/examples/graphql/consumer.ts new file mode 100644 index 000000000..a7f999859 --- /dev/null +++ b/examples/graphql/consumer.ts @@ -0,0 +1,30 @@ +import { ApolloClient, HttpLink } from "apollo-boost"; +import { InMemoryCache } from "apollo-cache-inmemory"; +import gql from "graphql-tag"; +import { createHttpLink } from "apollo-link-http"; + +const client = new ApolloClient({ + cache: new InMemoryCache(), + link: createHttpLink({ + fetch: require("node-fetch"), + headers: { + foo: "bar", + }, + uri: "http://localhost:4000/graphql", + }), +}); + +export function query(): any { + return client + .query({ + query: gql` + { + hello + } + `, + variables: { + foo: "bar", + }, + }) + .then((result: any) => result.data); +} diff --git a/examples/graphql/foo.js b/examples/graphql/foo.js new file mode 100644 index 000000000..477de064e --- /dev/null +++ b/examples/graphql/foo.js @@ -0,0 +1 @@ +{"description":"a hello request","response":{"body":{"data":{"hello":{"contents":"Hello world!","json_class":"Pact::SomethingLike"}}},"headers":{"Content-Type":"application/json; charset=utf-8"},"status":200},"request":{"body":{"operationName":"query","query":"{\n hello\n}\n","variables":{"foo":"bar"}},"headers":{"content-type":"application/json"},"method":"POST"}} diff --git a/examples/graphql/package.json b/examples/graphql/package.json index fec9a9c49..eb8ea2efc 100644 --- a/examples/graphql/package.json +++ b/examples/graphql/package.json @@ -4,8 +4,12 @@ "description": "", "main": "index.js", "scripts": { - "test:consumer": "mocha consumer.spec.js", - "test:provider": "mocha provider.spec.js" + "build": "tsc", + "clean": "if [ -d 'pacts' ]; then rm -rf pacts; fi", + "test": "npm run clean && npm run test:consumer && npm run test:publish && npm run test:provider", + "test:consumer": "nyc --check-coverage --reporter=html --reporter=text-summary mocha consumer.spec.ts", + "test:publish": "node publish.js", + "test:provider": "nyc --check-coverage --reporter=html --reporter=text-summary mocha -t 10000 provider.spec.ts" }, "keywords": [ "graphql", @@ -15,20 +19,23 @@ "author": "Matt Fellows ", "license": "ISC", "dependencies": { + "@types/chai-as-promised": "^7.1.0", "apollo-boost": "^0.1.4", "apollo-client": "^2.2.8", "apollo-client-preset": "^1.0.8", "apollo-server-express": "^1.3.5", "body-parser": "^1.18.2", + "chai-as-promised": "^7.1.1", "express": "^4.16.3", "express-graphql": "^0.6.12", "graphql": "^0.13.2", "graphql-tag": "^2.9.1", - "graphql-tools": "^2.24.0", - "react-apollo": "^2.1.3" + "graphql-tools": "^2.24.0" }, "devDependencies": { + "@types/mocha": "^2.2.41", "chai": "^4.1.2", - "mocha": "^5.1.1" + "mocha": "^5.1.1", + "nyc": "^11.6.0" } } diff --git a/examples/graphql/provider.js b/examples/graphql/provider.js deleted file mode 100644 index 2c201fa0f..000000000 --- a/examples/graphql/provider.js +++ /dev/null @@ -1,29 +0,0 @@ -const express = require('express'); -const graphqlHTTP = require('express-graphql'); -const { buildSchema } = require('graphql'); - -const schema = buildSchema(` - type Query { - hello: String - } -`); - -const root = { - hello: () => 'Hello world!' -}; - -const app = express(); -app.use('/graphql', graphqlHTTP({ - schema: schema, - rootValue: root, - graphiql: true, -})); - -const start = () => { - app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); -} - -module.exports = { - start, - app -} diff --git a/examples/graphql/provider.spec.js b/examples/graphql/provider.spec.js deleted file mode 100644 index 31239e3d5..000000000 --- a/examples/graphql/provider.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -const { - Verifier -} = require('../../dist/pact') -const path = require('path') -const chai = require('chai') -const chaiAsPromised = require('chai-as-promised') -const expect = chai.expect -const { VerifierOptions } = require('@pact-foundation/pact-node'); -const { app } = require('./provider.js') - -chai.use(chaiAsPromised) -const server = app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); - -// Verify that the provider meets all consumer expectations -describe('Pact Verification', () => { - it('should validate the expectations of Matching Service', function () { // lexical binding required here - let opts = { - provider: 'GraphQLProvider', - providerBaseUrl: 'http://localhost:4000/graphql', - // Local pacts - pactUrls: [path.resolve(process.cwd(), './pacts/graphqlconsumer-graphqlconsumerprovider.json')], - providerVersion: "1.0.0", - } - - return new Verifier().verifyProvider(opts) - .then(output => { - console.log('Pact Verification Complete!') - console.log(output) - server.close(); - }) - }) -}) diff --git a/examples/graphql/provider.spec.ts b/examples/graphql/provider.spec.ts new file mode 100644 index 000000000..3b4bdee9b --- /dev/null +++ b/examples/graphql/provider.spec.ts @@ -0,0 +1,37 @@ +import { Verifier } from "../../src/pact"; +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; + +import { VerifierOptions } from "@pact-foundation/pact-node"; +import app from "./provider"; + +const expect = chai.expect; +const path = require("path"); +chai.use(chaiAsPromised); + +const server = app.listen(4000, () => console.log("Now browse to localhost:4000/graphql")); + +// Verify that the provider meets all consumer expectations +describe("Pact Verification", () => { + it("should validate the expectations of Matching Service", () => { // lexical binding required here + const opts = { + provider: "GraphQLProvider", + providerBaseUrl: "http://localhost:4000/graphql", + // Local pacts + // pactUrls: [path.resolve(process.cwd(), "./pacts/graphqlconsumer-graphqlprovider.json")], + pactBrokerUrl: "https://test.pact.dius.com.au/", + pactBrokerUsername: "dXfltyFMgNOFZAxr8io9wJ37iUpY42M", + pactBrokerPassword: "O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1", + publishVerificationResult: true, + providerVersion: "1.0.0", + tags: ["prod"], + }; + + return new Verifier().verifyProvider(opts) + .then((output) => { + console.log("Pact Verification Complete!"); + console.log(output); + server.close(); + }); + }); +}); diff --git a/examples/graphql/provider.ts b/examples/graphql/provider.ts new file mode 100644 index 000000000..2f5e2c8fb --- /dev/null +++ b/examples/graphql/provider.ts @@ -0,0 +1,26 @@ +const express = require("express"); +const graphqlHTTP = require("express-graphql"); +const { buildSchema } = require("graphql"); + +const schema = buildSchema(` + type Query { + hello: String + } +`); + +const root = { + hello: () => "Hello world!", +}; + +const app = express(); +export default app; + +app.use("/graphql", graphqlHTTP({ + graphiql: true, + rootValue: root, + schema, +})); + +export function start(): any { + app.listen(4000, () => console.log("Now browse to localhost:4000/graphql")); +} diff --git a/examples/graphql/publish.js b/examples/graphql/publish.js new file mode 100644 index 000000000..becbdf7fc --- /dev/null +++ b/examples/graphql/publish.js @@ -0,0 +1,23 @@ +const pact = require('@pact-foundation/pact-node') +const path = require('path') +const opts = { + pactFilesOrDirs: [path.resolve(__dirname, 'pacts/graphqlconsumer-graphqlprovider.json')], + pactBroker: 'https://test.pact.dius.com.au', + pactBrokerUsername: 'dXfltyFMgNOFZAxr8io9wJ37iUpY42M', + pactBrokerPassword: 'O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1', + tags: ['prod', 'test'], + consumerVersion: '1.0.' + ((process.env.TRAVIS_BUILD_NUMBER) ? process.env.TRAVIS_BUILD_NUMBER : Math.floor(new Date() / 1000)) +} + +pact.publishPacts(opts) + .then(() => { + console.log('Pact contract publishing complete!') + console.log('') + console.log('Head over to https://test.pact.dius.com.au/ and login with') + console.log('=> Username: dXfltyFMgNOFZAxr8io9wJ37iUpY42M') + console.log('=> Password: O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1') + console.log('to see your published contracts.') + }) + .catch(e => { + console.log('Pact contract publishing failed: ', e) + }) diff --git a/examples/messages/publish.js b/examples/messages/publish.js index 369316b90..cf0cb3f9a 100644 --- a/examples/messages/publish.js +++ b/examples/messages/publish.js @@ -6,7 +6,7 @@ const opts = { pactBrokerUsername: 'dXfltyFMgNOFZAxr8io9wJ37iUpY42M', pactBrokerPassword: 'O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1', tags: ['prod', 'test'], - consumerVersion: '1.0.1' + consumerVersion: '1.0.' + ((process.env.TRAVIS_BUILD_NUMBER) ? process.env.TRAVIS_BUILD_NUMBER : Math.floor(new Date() / 1000)) } pact.publishPacts(opts) diff --git a/examples/serverless/publish.js b/examples/serverless/publish.js index a4e378f30..27fa06fa3 100644 --- a/examples/serverless/publish.js +++ b/examples/serverless/publish.js @@ -6,7 +6,7 @@ const opts = { pactBrokerUsername: 'dXfltyFMgNOFZAxr8io9wJ37iUpY42M', pactBrokerPassword: 'O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1', tags: ['latest'], - consumerVersion: '1.0.1' + consumerVersion: '1.0.' + ((process.env.TRAVIS_BUILD_NUMBER) ? process.env.TRAVIS_BUILD_NUMBER : Math.floor(new Date() / 1000)) } pact.publishPacts(opts) diff --git a/package.json b/package.json index 1c6bf5956..83fddb29f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "predist": "npm run clean && npm run lint && npm run jscpd", "release": "standard-version --prerelease alpha --release-as patch", "test": "nyc --check-coverage --reporter=html --reporter=text-summary mocha", - "test:examples": "npm run test:e2e-examples && npm run test:jest-examples && npm run test:mocha-examples && npm run test:ava-examples && npm run test:ts-examples && npm run test:message-examples && npm run test:serverless-examples", + "test:examples": "npm run test:e2e-examples && npm run test:jest-examples && npm run test:mocha-examples && npm run test:ava-examples && npm run test:ts-examples && npm run test:message-examples && npm run test:serverless-examples && npm run test:graphql-examples", "test:e2e-examples": "cd examples/e2e && npm i && npm t", "test:ava-examples": "cd examples/ava && npm i && npm t", "test:jest-examples": "cd examples/jest && npm i && npm t", @@ -26,6 +26,7 @@ "test:ts-examples": "cd examples/typescript && npm i && npm t", "test:message-examples": "cd examples/messages && npm i && npm t", "test:serverless-examples": "cd examples/serverless && npm i && npm t", + "test:graphql-examples": "cd examples/graphql && npm i && npm t", "test:karma": "npm run test:karma:jasmine && npm run test:karma:mocha", "test:karma:jasmine": "karma start ./karma/jasmine/karma.conf.js", "test:karma:mocha": "karma start ./karma/mocha/karma.conf.js", @@ -49,8 +50,7 @@ "consumer driven testing" ], "author": "Matt Fellows (http://twitter.com/matthewfellows)", - "contributors": [ - { + "contributors": [{ "name": "Tarcio Saraiva", "email": "tarcio@gmail.com", "url": "http://twitter.com/tarciosaraiva" @@ -90,6 +90,8 @@ "es6-object-assign": "^1.1.0", "es6-promise": "^4.1.1", "express": "^4.16.3", + "graphql": "^0.13.2", + "graphql-tag": "^2.9.1", "lodash": "^4.17.4", "lodash.isfunction": "3.0.8", "lodash.isnil": "4.0.0", diff --git a/src/dsl/interaction.spec.ts b/src/dsl/interaction.spec.ts index c7557e41c..8984c4f85 100644 --- a/src/dsl/interaction.spec.ts +++ b/src/dsl/interaction.spec.ts @@ -61,7 +61,10 @@ describe("Interaction", () => { }); describe("with only mandatory params", () => { - const actual = new Interaction().withRequest({ method: HTTPMethod.GET, path: "/search" }).json(); + const actual = new Interaction() + .uponReceiving("a request") + .withRequest({ method: HTTPMethod.GET, path: "/search" }) + .json(); it("has a state containing only the given keys", () => { expect(actual).to.have.keys("request"); @@ -74,13 +77,15 @@ describe("Interaction", () => { }); describe("with all other parameters", () => { - const actual = new Interaction().withRequest({ - body: { id: 1, name: "Test", due: "tomorrow" }, - headers: { "Content-Type": "application/json" }, - method: HTTPMethod.GET, - path: "/search", - query: "q=test", - }).json(); + const actual = new Interaction() + .uponReceiving("request") + .withRequest({ + body: { id: 1, name: "Test", due: "tomorrow" }, + headers: { "Content-Type": "application/json" }, + method: HTTPMethod.GET, + path: "/search", + query: "q=test", + }).json(); it("has a full state all available keys", () => { expect(actual).to.have.keys("request"); @@ -103,6 +108,7 @@ describe("Interaction", () => { describe("with only mandatory params", () => { interaction = new Interaction(); + interaction.uponReceiving("request") interaction.willRespondWith({ status: 200 }); const actual = interaction.json(); @@ -118,6 +124,7 @@ describe("Interaction", () => { describe("with all other parameters", () => { interaction = new Interaction(); + interaction.uponReceiving("request") interaction.willRespondWith({ body: { id: 1, name: "Test", due: "tomorrow" }, headers: { "Content-Type": "application/json" }, diff --git a/src/dsl/interaction.ts b/src/dsl/interaction.ts index 461857b3c..d8f527daf 100644 --- a/src/dsl/interaction.ts +++ b/src/dsl/interaction.ts @@ -7,10 +7,12 @@ import { isNil, keys, omitBy } from "lodash"; import { HTTPMethod, methods } from "../common/request"; import { MatcherResult } from "./matchers"; +export type Query = string | { [name: string]: string | MatcherResult }; + export interface RequestOptions { method: HTTPMethod | methods; path: string | MatcherResult; - query?: any; + query?: Query; headers?: { [name: string]: string | MatcherResult }; body?: any; } @@ -36,7 +38,7 @@ export interface InteractionState { } export class Interaction { - private state: InteractionState = {}; + protected state: InteractionState = {}; /** * Gives a state the provider should be in for this interaction. @@ -118,6 +120,9 @@ export class Interaction { * @returns {Object} */ public json(): InteractionState { + if (isNil(this.state.description)) { + throw new Error("You must provide a description for the Interaction"); + } return this.state; } } diff --git a/src/dsl/utils.ts b/src/dsl/utils.ts index c5261ee2b..a021a92d7 100644 --- a/src/dsl/utils.ts +++ b/src/dsl/utils.ts @@ -1,85 +1,117 @@ -// TODO: potentially make a number of client interfaces -// e.g. ApolloClient -import { Interaction, ResponseOptions, RequestOptions } from "../pact"; -import { HTTPMethod, methods } from "common/request"; -import { MatcherResult } from "./matchers"; -import { extend } from "underscore"; - /** * Pact Utilities module. * @module PactUtils */ +import { Interaction, ResponseOptions, RequestOptions, InteractionState, Query } from "../dsl/interaction"; +import { HTTPMethod, methods } from "../common/request"; +import { MatcherResult, regex } from "./matchers"; +import { extend } from "underscore"; +import { keys, isNil, omitBy } from "lodash"; +import gql from "graphql-tag"; +import { ERROR } from "bunyan"; -export interface GraphQLRequestOptions { - // Overwite default http headers - headers?: { [name: string]: string | MatcherResult }; - - // Overwrite HTTP method. - // Defaults to "POST" - method?: HTTPMethod | methods; - - // API endpoint - path: string | MatcherResult; - - // Query strings - query?: any; +export type GraphQLOperation = "query" | "mutation" | null; - // Only specify if you want to overwrite the default - // GraphQL Query-to-Pact converter - body?: any; +enum GraphQLOperations { + query = "query", + mutation = "mutation", } +export interface GraphQLVariables { [name: string]: any; } + /** * GraphQL interface */ -export interface GraphQLQuery { - description: string; - - // Provider State - state?: string; - - // The name of the consumer - operation: "query" | "mutation" | null; - - // GraphQL query body - // e.g. the value for the query field in: - // '{ "query": "{ - // Category(id:7) { - // id, - // name, - // subcategories { - // id, - // name - // } - // } - // }" - // }' - query: string; - - variables: { [name: string]: any }; - - // Specify the HTTP options for the GraphQL Query - requestOptions: GraphQLRequestOptions; -} +export class GraphQLInteraction extends Interaction { + private operation: GraphQLOperation = null; + private variables: GraphQLVariables = {}; + private query: string; + + /** + * The type of GraphQL operation. Generally not required. + * + * @param {string} operation The operation, one of "query"|"mutation" + * @returns {Interaction} interaction + */ + public withOperation(operation: GraphQLOperation) { + if (!operation || operation && keys(GraphQLOperations).indexOf(operation.toString()) < 0) { + throw new Error(`You must provide a valid HTTP method: ${keys(GraphQLOperations).join(", ")}.`); + } -export function graphql(query: GraphQLQuery, response: ResponseOptions): Interaction { - const interaction = new Interaction(); - const request: RequestOptions = extend({ - body: { - operationName: query.operation, - query: query.query, - variables: query.variables, - }, - headers: { "content-type": "application/json" }, - method: "POST", - }, query.requestOptions); - - if (query.state) { - interaction.given(query.state); + this.operation = operation; + + return this; } - interaction.uponReceiving(query.description); - interaction.withRequest(request); - interaction.willRespondWith(response); - return interaction; + /** + * Any variables used in the Query + * @param {Object} variables a k/v set of variables for the query + * @returns {Interaction} interaction + */ + public withVariables(variables: GraphQLVariables) { + this.variables = variables; + + return this; + } + + /** + * The actual GraphQL query as a string. + * + * NOTE: spaces are not important, Pact will auto-generate a space-insensitive matcher + * + * e.g. the value for the "query" field in the GraphQL HTTP payload: + * '{ "query": "{ + * Category(id:7) { + * id, + * name, + * subcategories { + * id, + * name + * } + * } + * }" + * }' + * @param {Object} query the actual GraphQL query, as per example above. + * @returns {Interaction} interaction + */ + public withQuery(query: string) { + if (isNil(query)) { + throw new Error("You must provide a GraphQL query."); + } + + try { + gql(query); + } catch (e) { + throw new Error(`GraphQL Query is invalid: ${e.message}`); + } + + this.query = query; + + return this; + } + + /** + * Returns the interaction object created. + * @returns {Object} + */ + public json(): InteractionState { + if (isNil(this.query)) { + throw new Error("You must provide a GraphQL query."); + } + if (isNil(this.state.description)) { + throw new Error("You must provide a description for the query."); + } + + this.state.request = extend({ + body: { + operationName: this.operation, + query: regex({ generate: this.query, matcher: this.query.replace(/\s+/g, "\\s+") }), + variables: this.variables, + }, + headers: { "content-type": "application/json" }, + method: "POST", + }, this.state.request); + + return this.state; + } }