-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from symm/exclude-directives
Add option for excluding directives
- Loading branch information
Showing
9 changed files
with
154 additions
and
107 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -19,19 +22,22 @@ | |
"author": "Gareth Jones <[email protected]>", | ||
"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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": [] | ||
} |