Skip to content

Commit

Permalink
Merge pull request #3 from symm/exclude-directives
Browse files Browse the repository at this point in the history
Add option for excluding directives
  • Loading branch information
symm committed May 21, 2018
2 parents 953d674 + 7d83758 commit 5e4b939
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 107 deletions.
15 changes: 0 additions & 15 deletions .eslintrc.js

This file was deleted.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ Consumer driven contract testing for GraphQL APIs.
## Usage

```
GraphQL Contract Test v0.0.8
GraphQL Contract Test v0.0.10
Check if the remote server fulfills the supplied GraphQL contract file
Usage: graphql-contract-test ENDPOINT_URL client_schema_file
Options:
--header, -h Add a custom header (ex. 'Authorization=Bearer ...'), can be used multiple times
--header, -h Add a custom header (ex. 'Authorization=Bearer ...'), can be used multiple times
--ignore-directives Exclude directive changes from the comparison
```

Where `client-schema.graphql` contains the schema you expect the server to implement.
Expand Down
17 changes: 17 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
}
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
"README.md",
"dist"
],
"version": "0.0.9",
"version": "0.0.10",
"description": "Contract test GraphQL endpoint using a Schema file",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"prepublish": "npm run build && chmod +x dist/index.js",
"test": "jest"
"test": "jest",
"test:watch": "jest --watch"
},
"repository": {
"type": "git",
Expand All @@ -19,19 +22,22 @@
"author": "Gareth Jones <me@gazj.co.uk>",
"license": "MIT",
"dependencies": {
"@types/chalk": "^0.4.31",
"@types/graphql": "^0.13.0",
"@types/minimist": "^1.2.0",
"@types/node": "^7.0.59",
"@types/node-fetch": "^1.6.8",
"chalk": "^1.1.3",
"cli-table2": "^0.2.0",
"graphql": "^0.13.0",
"minimist": "^1.2.0",
"node-fetch": "^1.6.3"
},
"devDependencies": {
"@types/chalk": "^0.4.31",
"@types/graphql": "^0.13.0",
"@types/jest": "^22.2.3",
"@types/minimist": "^1.2.0",
"@types/node": "^7.0.59",
"@types/node-fetch": "^1.6.8",
"jest": "^21.2.1",
"ts-jest": "^22.4.6",
"tslint": "^5.10.0",
"typescript": "^2.8.1"
}
}
141 changes: 77 additions & 64 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,121 +1,134 @@
#!/usr/bin/env node

import fetch from 'node-fetch'
import {introspectionQuery} from 'graphql/utilities/introspectionQuery'
import {buildClientSchema} from 'graphql/utilities/buildClientSchema'
import * as minimist from 'minimist'
import * as chalk from 'chalk'
import * as fs from "fs";
import {buildSchema, GraphQLSchema} from "graphql";
import {findBreakingChanges} from "graphql/utilities";
import {transformChangeType} from "./utils/transformChangeType";
import {transformChangeDescription} from "./utils/transformChangeDescription";
import Table = require('cli-table2')

const {version, name} = require('../package.json');
import {
BreakingChange,
buildClientSchema,
buildSchema,
findBreakingChanges,
GraphQLSchema,
introspectionQuery,
} from "graphql"

import fetch from "node-fetch"

import * as chalk from "chalk"
import * as fs from "fs"
import * as minimist from "minimist"

import {transformChangeDescription} from "./utils/transformChangeDescription"
import {transformChangeType} from "./utils/transformChangeType"

import * as CliTable2 from "cli-table2"

const name = "graphql-contract-test"
const version = "0.0.10"

const usage = `
${chalk.bold(
'Check if the remote server fulfills the supplied GraphQL contract file',
"Check if the remote server fulfills the supplied GraphQL contract file",
)}
Usage: graphql-contract-test ENDPOINT_URL client_schema_file
Options:
--header, -h Add a custom header (ex. 'Authorization=Bearer ...'), can be used multiple times
`;
--header, -h Add a custom header (ex. 'Authorization=Bearer ...'), can be used multiple times
--ignore-directives Exclude directive changes from the comparison
`

const intro = ` GraphQL Contract Test v${version}
`;
`

async function main() {
console.log(intro);
async function main(): Promise<void> {
console.log(intro)

const argv = minimist(process.argv.slice(2));
const argv = minimist(process.argv.slice(2))

if (argv._.length < 2) {
console.log(usage);
process.exit(1);
console.log(usage)
process.exit(1)
}

const endpoint = argv._[0];
const contractFile = argv._[1];
const headers = parseHeaderOptions(argv);
const endpoint = argv._[0]
const contractFile = argv._[1]
const headers = parseHeaderOptions(argv)

const implementation = await getImplementedSchema(endpoint, headers);
const contract = getContractSchema(contractFile);
const implementation = await getImplementedSchema(endpoint, headers)
const contract = getContractSchema(contractFile)

const breakingChanges = findBreakingChanges(contract, implementation);
let breakingChanges = findBreakingChanges(contract, implementation)

if (argv["ignore-directives"]) {
console.log(" ❗️ Ignoring directive differences")
breakingChanges = breakingChanges.filter((breakingChange) => breakingChange.type !== "DIRECTIVE_REMOVED")
}

if (breakingChanges.length === 0) {
console.log(chalk.bold.green(' ✨ The server appears to implement the schema you provided'));
process.exit(0);
console.log(chalk.bold.green(" ✨ The server appears to implement the schema you provided"))
process.exit(0)
}

console.log(` 💩 ${chalk.bold.red('Breaking changes were detected\n')}`);
console.log(` 💩 ${chalk.bold.red("Breaking changes were detected\n")}`)

const table = buildResultsTable(breakingChanges);
console.log(table.toString());
const table = buildResultsTable(breakingChanges)
console.log(table.toString())

process.exit(1)
}

function buildResultsTable(breakingChanges): Table {
const table = new Table({
head: ['Issue', 'Description'],
});
function buildResultsTable(breakingChanges: BreakingChange[]) {
const table = new CliTable2({
head: ["Issue", "Description"],
})

breakingChanges.forEach((change) => {
table.push([transformChangeType(change.type), transformChangeDescription(change.description)]);
});
table.push([transformChangeType(change.type), transformChangeDescription(change.description)])
})

return table
}

function getContractSchema(expectedSchemaFile): GraphQLSchema
{
const data = fs.readFileSync(expectedSchemaFile, 'utf8');
function getContractSchema(expectedSchemaFile: string): GraphQLSchema {
const data = fs.readFileSync(expectedSchemaFile, "utf8")

return buildSchema(data)
}

async function getImplementedSchema(endpoint, headers): Promise<GraphQLSchema>
{
async function getImplementedSchema(endpoint: string, headers: string[]): Promise<GraphQLSchema> {
const response = await fetch(endpoint, {
method: 'POST',
headers: headers,
body: JSON.stringify({query: introspectionQuery}),
});
body: JSON.stringify({query: introspectionQuery}),
headers,
method: "POST",
})

const {data, errors} = await response.json();
const {data, errors} = await response.json()

if (errors) {
throw new Error(JSON.stringify(errors, null, 2))
}

return buildClientSchema(data);
return buildClientSchema(data)
}

function parseHeaderOptions(argv) {
function parseHeaderOptions(argv: minimist.ParsedArgs): string[] {
const defaultHeaders = {
'Content-Type': 'application/json',
'User-Agent': `${name} v${version}`
};
"Content-Type": "application/json",
"User-Agent": `${name} v${version}`,
}

return toArray(argv['header'])
.concat(toArray(argv['h']))
return toArray(argv.header)
.concat(toArray(argv.h))
.reduce((obj, header: string) => {
const [key, value] = header.split('=');
obj[key] = value;
const [key, value] = header.split("=")
obj[key] = value
return obj
}, defaultHeaders);
}, defaultHeaders)
}

function toArray(value = []) {
function toArray(value = []): any[] {
return Array.isArray(value) ? value : [value]
}

main().catch(e => {
console.log(`${chalk.bold.red(e.message)}`);
process.exit(1);
});
main().catch((e) => {
console.log(`${chalk.bold.red(e.message)}`)
process.exit(1)
})
13 changes: 13 additions & 0 deletions src/utils/__test__/transformChangeType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {transformChangeType} from "../transformChangeType"

test("Transforms change constants into human readable format", () => {
expect(transformChangeType("FIELD_CHANGED_KIND")).toBe("Field is the wrong kind")
expect(transformChangeType("FIELD_REMOVED")).toBe("Field does not exist")
expect(transformChangeType("TYPE_REMOVED")).toBe("Type does not exist")
expect(transformChangeType("TYPE_REMOVED_FROM_UNION")).toBe("Type not in Union")
expect(transformChangeType("VALUE_REMOVED_FROM_ENUM")).toBe("Value not in ENUM")
expect(transformChangeType("ARG_REMOVED")).toBe("Arg not present")
expect(transformChangeType("ARG_CHANGED_KIND")).toBe("Argument is the wrong kind")
expect(transformChangeType("INTERFACE_REMOVED_FROM_OBJECT")).toBe("Interface not present on object")
expect(transformChangeType("ARG_DEFAULT_VALUE_CHANGE")).toBe("Argument default value does not match")
})
10 changes: 5 additions & 5 deletions src/utils/transformChangeDescription.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export function transformChangeDescription(message) {
let transformed = message.replace(' was removed.', ' is not present.');
export function transformChangeDescription(message: string): string {
let transformed = message.replace(" was removed.", " is not present.")

transformed = transformed.replace(
/changed type from (.*) to (.*)./,
(match, p1, p2) => `expected ${p1} but got ${p2}.`,
);
)

return transformed;
}
return transformed
}
28 changes: 14 additions & 14 deletions src/utils/transformChangeType.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
export const BreakingChangeMapping = {
FIELD_CHANGED_KIND: 'Field is the wrong kind',
FIELD_REMOVED: 'Field does not exist',
TYPE_CHANGED_KIND: 'Type is the wrong kind',
TYPE_REMOVED: 'Type does not exist',
TYPE_REMOVED_FROM_UNION: 'Type not in Union',
VALUE_REMOVED_FROM_ENUM: 'Value not in ENUM',
ARG_REMOVED: 'Arg not present',
ARG_CHANGED_KIND: 'Argument is the wrong kind',
INTERFACE_REMOVED_FROM_OBJECT: 'Interface not present on object',
ARG_DEFAULT_VALUE_CHANGE: 'Argument default value does not match',
};
ARG_CHANGED_KIND: "Argument is the wrong kind",
ARG_DEFAULT_VALUE_CHANGE: "Argument default value does not match",
ARG_REMOVED: "Arg not present",
FIELD_CHANGED_KIND: "Field is the wrong kind",
FIELD_REMOVED: "Field does not exist",
INTERFACE_REMOVED_FROM_OBJECT: "Interface not present on object",
TYPE_CHANGED_KIND: "Type is the wrong kind",
TYPE_REMOVED: "Type does not exist",
TYPE_REMOVED_FROM_UNION: "Type not in Union",
VALUE_REMOVED_FROM_ENUM: "Value not in ENUM",
}

export function transformChangeType(message) {
return BreakingChangeMapping[message];
}
export function transformChangeType(message: string): string {
return BreakingChangeMapping[message]
}
12 changes: 12 additions & 0 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"semicolon": [true, "never"],
"no-console": [false]
},
"rulesDirectory": []
}

0 comments on commit 5e4b939

Please sign in to comment.