From 084b6d3428469a6087921b23a750993c5ce9fcfa Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Sun, 20 Aug 2017 23:05:32 +1000 Subject: [PATCH] wip: convert main classes to crude TS implementation --- .editorconfig | 3 + README.md | 8 +- package-lock.json | 166 +++++++++++++++++++++ package.json | 12 +- src/common/request.d.ts | 4 +- src/dsl/interaction.d.ts | 32 ---- src/dsl/interaction.js | 118 --------------- src/dsl/interaction.ts | 137 +++++++++++++++++ src/dsl/matchers.d.ts | 26 ---- src/dsl/{matchers.js => matchers.ts} | 34 ++--- src/dsl/mockService.d.ts | 18 --- src/dsl/mockService.js | 77 ---------- src/dsl/mockService.ts | 85 +++++++++++ src/dsl/verifier.d.ts | 2 - src/dsl/verifier.js | 12 -- src/dsl/verifier.ts | 9 ++ src/pact.d.ts | 38 ----- src/pact.js | 167 --------------------- src/pact.ts | 212 +++++++++++++++++++++++++++ src/types/custom.d.ts | 3 + test/mocha.opts | 1 + test/pact.spec.ts | 106 ++++++++++++++ tsconfig.json | 3 + 23 files changed, 759 insertions(+), 514 deletions(-) delete mode 100644 src/dsl/interaction.d.ts delete mode 100644 src/dsl/interaction.js create mode 100644 src/dsl/interaction.ts delete mode 100644 src/dsl/matchers.d.ts rename src/dsl/{matchers.js => matchers.ts} (70%) delete mode 100644 src/dsl/mockService.d.ts delete mode 100644 src/dsl/mockService.js create mode 100644 src/dsl/mockService.ts delete mode 100644 src/dsl/verifier.d.ts delete mode 100644 src/dsl/verifier.js create mode 100644 src/dsl/verifier.ts delete mode 100644 src/pact.d.ts delete mode 100644 src/pact.js create mode 100644 src/pact.ts create mode 100644 src/types/custom.d.ts create mode 100644 test/pact.spec.ts diff --git a/.editorconfig b/.editorconfig index ec1b6ce00..7c48bff3b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,9 +5,12 @@ root = true # Unix-style newlines with a newline ending every file [*] +charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true +indent_style = space +indent_size = 2 # Matches multiple files with brace expansion notation # Set default charset diff --git a/README.md b/README.md index df19696a3..2bbd68edd 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ For simplicity, we alias the main matches to make our code more readable: The underlying mock service is written in Ruby, so the regular expression must be in a Ruby format, not a Javascript format. ```javascript -const { term } = pact.Matchers +const { term } = pact provider.addInteraction({ state: 'Has some animals', @@ -343,7 +343,7 @@ provider.addInteraction({ #### Match based on type ```javascript -const { somethingLike: like } = pact.Matchers +const { like } = pact provider.addInteraction({ state: 'Has some animals', @@ -379,7 +379,7 @@ Note that you can wrap a `like` around a single value or an object. When wrapped Matching provides the ability to specify flexible length arrays. For example: ```javascript -pact.Matchers.eachLike(obj, { min: 3 }) +pact.eachLike(obj, { min: 3 }) ``` Where `obj` can be any javascript object, value or Pact.Match. It takes optional argument (`{ min: 3 }`) where min is greater than 0 and defaults to 1 if not provided. @@ -387,7 +387,7 @@ Where `obj` can be any javascript object, value or Pact.Match. It takes optional Below is an example that uses all of the Pact Matchers. ```javascript -const { somethingLike: like, term, eachLike } = pact.Matchers +const { somethingLike: like, term, eachLike } = pact const animalBodyExpectation = { 'id': 1, diff --git a/package-lock.json b/package-lock.json index 7885c69c6..04579516a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,6 +104,51 @@ "integrity": "sha1-ZMbV2NEk+X1bRA8sCQq14jZ/YjY=", "optional": true }, + "@types/chai": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.3.tgz", + "integrity": "sha512-AkwKxZJ3ml5OsKi0kNTzGBANM3vwcnNS8nu/geVFaM55raUQ8Gz5MhPuu2cQqhf3h829tFfIGD1pbZSc2lNJeg==", + "dev": true + }, + "@types/cli-color": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@types/cli-color/-/cli-color-0.3.29.tgz", + "integrity": "sha1-yDpx/gLIx+HM7ASN1qJFjR9sluo=", + "dev": true + }, + "@types/lodash": { + "version": "4.14.73", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.73.tgz", + "integrity": "sha512-wZDQC6C2VlCaddkd57b363vLL8J6zcwMqP5jqR4Aikcfh85FmPTINrSCWXZHG9JlkQ07ojeNNt71EyccfIdnKQ==", + "dev": true + }, + "@types/lodash.isnil": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/lodash.isnil/-/lodash.isnil-4.0.3.tgz", + "integrity": "sha512-ImgV11wqzY9en55SFEZdvuT7hZAcoFdwczMdcjiBJGC0xfpWh3UzzfvW+yAldtIDiaApHU4hu0iIcusGv57ALg==", + "dev": true, + "requires": { + "@types/lodash": "4.14.73" + } + }, + "@types/mocha": { + "version": "2.2.41", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.41.tgz", + "integrity": "sha1-4nzwgXFT658nE7LT9saPHhw8pgg=", + "dev": true + }, + "@types/proxyquire": { + "version": "1.3.27", + "resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.27.tgz", + "integrity": "sha1-njbU88Ka8dQI/NIdpOKvV8V3sGY=", + "dev": true + }, + "@types/sinon": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-2.3.3.tgz", + "integrity": "sha512-bnoHhhCsx0p0yhLOywFg6T7Le37JjtnzLcWal6cuSPvIZUBzKRIsqM6E5OsKUIRVErCaBCghHIZmqtyGk5uXyA==", + "dev": true + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -2014,6 +2059,21 @@ "integrity": "sha1-S5BvZw5aljqHt2sOFolkM0G2Ajw=", "dev": true }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "colors": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", @@ -6101,6 +6161,12 @@ "pify": "2.3.0" } }, + "make-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", + "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", + "dev": true + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -8707,6 +8773,100 @@ "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", "dev": true }, + "ts-node": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", + "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "chalk": "2.1.0", + "diff": "3.3.0", + "make-error": "1.3.0", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.4.15", + "tsconfig": "6.0.0", + "v8flags": "3.0.0", + "yn": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.1" + } + }, + "diff": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", + "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "supports-color": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", + "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "v8flags": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.0.tgz", + "integrity": "sha512-AGl+C+4qpeSu2g3JxCD/mGFFOs/vVZ3XREkD3ibQXEqr4Y4zgIrPWW124/IKJFHOIVFIoH8miWrLf0o84HYjwA==", + "dev": true, + "requires": { + "user-home": "1.1.1" + } + } + } + }, + "tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "dev": true, + "requires": { + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "tslib": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", @@ -9723,6 +9883,12 @@ "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true } } } diff --git a/package.json b/package.json index f268e9053..5ff445b60 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Pact for all things Javascript", "main": "./src/pact.js", "types": "./src/pact.d.ts", + "typings": "./src/types/custom.d.ts", "scripts": { "build": "./node_modules/.bin/tsc", "clean": "rimraf docs dist dist-web coverage logs pacts jscpd.json", @@ -16,7 +17,8 @@ "postdist": "npm t", "posttest": "npm run test:checkCoverage && npm run test:karma && npm run docs", "predist": "npm run clean && npm run lint && npm run jscpd", - "test": "istanbul cover ./node_modules/.bin/_mocha --timeout 10000 -- ./test", + "test": "istanbul cover ./node_modules/.bin/_mocha -- ./test/**/*.js", + "test:typescript": "istanbul cover ./node_modules/.bin/_mocha -- ./test/**/*.ts", "test:checkCoverage": "istanbul check", "test:e2e-examples": "cd examples/e2e && npm i && npm t", "test:karma": "npm run test:karma:jasmine && npm run test:karma:mocha", @@ -81,6 +83,13 @@ "lodash.omitby": "4.6.0" }, "devDependencies": { + "@types/chai": "^4.0.3", + "@types/cli-color": "^0.3.29", + "@types/lodash": "^4.14.73", + "@types/lodash.isnil": "^4.0.3", + "@types/mocha": "^2.2.41", + "@types/proxyquire": "^1.3.27", + "@types/sinon": "^2.3.3", "awesome-typescript-loader": "^3.2.3", "babel-cli": "6.x", "babel-eslint": "6.x", @@ -112,6 +121,7 @@ "source-map-loader": "^0.2.1", "standard": "8.x", "superagent": "2.x", + "ts-node": "^3.3.0", "tslint": "^5.6.0", "typescript": "^2.4.2", "webpack": "^3.5.5" diff --git a/src/common/request.d.ts b/src/common/request.d.ts index aae628bc3..4ff7e1d68 100644 --- a/src/common/request.d.ts +++ b/src/common/request.d.ts @@ -1,6 +1,6 @@ export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; export class Request { - constructor (); - send (method: HTTPMethod, url: string, body: string): Promise; + constructor(); + send(method: HTTPMethod, url: string, body?: string): Promise; } diff --git a/src/dsl/interaction.d.ts b/src/dsl/interaction.d.ts deleted file mode 100644 index 72a84d49c..000000000 --- a/src/dsl/interaction.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {HTTPMethod} from '../common/request'; -import {MatcherResult} from './matchers'; - -export class Interaction { - constructor(); - given(providerState: string): Interaction; - uponReceiving(description: string): Interaction; - withRequest(requestOpts: RequestOptions): Interaction; - willRespondWith(responseOpts: ResponseOptions): Interaction; - json(): object; -} - -export interface RequestOptions { - method: HTTPMethod; - path: string | MatcherResult; - query?: any; - headers?: {[name: string]: string | MatcherResult}; - body?: any; -} - -export interface ResponseOptions { - status: number | MatcherResult; - headers?: {[name: string]: string | MatcherResult}; - body?: any; -} - -export interface InteractionObject { - state: string; - uponReceiving: string; - withRequest: RequestOptions; - willRespondWith: ResponseOptions; -} diff --git a/src/dsl/interaction.js b/src/dsl/interaction.js deleted file mode 100644 index ddd4f6d5d..000000000 --- a/src/dsl/interaction.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * An Interaction is where you define the state of your interaction with a Provider. - * @module Interaction - */ - -'use strict' - -const omitBy = require('lodash.omitby') -const isNil = require('lodash.isnil') - -const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] - -module.exports = class Interaction { - - /** - * Creates a new Interaction. - * @returns {Interaction} interaction - */ - constructor () { - this.state = {} - return this - } - - /** - * Gives a state the provider should be in for this interaction. - * @param {string} providerState - The state of the provider. - * @returns {Interaction} interaction - */ - given (providerState) { - if (providerState) { - this.state['providerState'] = providerState - } - return this - } - - /** - * A free style description of the interaction. - * @param {string} description - A description of the interaction. - * @returns {Interaction} interaction - */ - uponReceiving (description) { - if (isNil(description)) { - throw new Error('You must provide a description for the interaction.') - } - this.state['description'] = description - return this - } - - /** - * The request that represents this interaction triggered by the consumer. - * @param {Object} requestOpts - * @param {string} requestOpts.method - The HTTP method - * @param {string} requestOpts.path - The path of the URL - * @param {string} requestOpts.query - Any query string in the interaction - * @param {Object} requestOpts.headers - A key-value pair oject of headers - * @param {Object} requestOpts.body - The body, in {@link String} format or {@link Object} format - * @returns {Interaction} interaction - */ - withRequest (requestOpts) { - var method = requestOpts.method - var path = requestOpts.path - var query = requestOpts.query - var headers = requestOpts.headers - var body = requestOpts.body - - if (isNil(method)) { - throw new Error('You must provide a HTTP method.') - } - - if (VALID_METHODS.indexOf(method.toUpperCase()) < 0) { - throw new Error('You must provide a valid HTTP method.') - } - - if (isNil(path)) { - throw new Error('You must provide a path.') - } - - this.state['request'] = omitBy({ - method: method.toUpperCase(), - path: path, - query: query, - headers: headers, - body: body - }, isNil) - return this - } - - /** - * The response expected by the consumer. - * @param {Object} responseOpts - * @param {string} responseOpts.status - The HTTP status - * @param {string} responseOpts.headers - * @param {Object} responseOpts.body - */ - willRespondWith (responseOpts) { - var status = responseOpts.status - var headers = responseOpts.headers - var body = responseOpts.body - - if (isNil(status) || status.toString().trim().length === 0) { - throw new Error('You must provide a status code.') - } - - this.state['response'] = omitBy({ - status: status, - headers: headers || undefined, - body: body || undefined - }, isNil) - } - - /** - * Returns the interaction object created. - * @returns {Object} - */ - json () { - return this.state - } -} diff --git a/src/dsl/interaction.ts b/src/dsl/interaction.ts new file mode 100644 index 000000000..246c305bb --- /dev/null +++ b/src/dsl/interaction.ts @@ -0,0 +1,137 @@ +/** + * An Interaction is where you define the state of your interaction with a Provider. + * @module Interaction + */ + + +import { MatcherResult } from './matchers'; +import { HTTPMethod } from '../common/request'; +import { isNil, omitBy } from 'lodash'; + +export interface RequestOptions { + method: HTTPMethod; + path: string | MatcherResult; + query?: any; + headers?: { [name: string]: string | MatcherResult }; + body?: any; +} + +export interface ResponseOptions { + status: number | MatcherResult; + headers?: { [name: string]: string | MatcherResult }; + body?: any; +} + +export interface InteractionObject { + state: string; + uponReceiving: string; + withRequest: RequestOptions; + willRespondWith: ResponseOptions; +} + +export interface InteractionState { + providerState?: string; + description?: string; + request?: RequestOptions; + response?: ResponseOptions; +} + +export class Interaction { + private state: InteractionState = {}; + + /** + * Creates a new Interaction. + * @returns {Interaction} interaction + */ + constructor() { } + + /** + * Gives a state the provider should be in for this interaction. + * @param {string} providerState - The state of the provider. + * @returns {Interaction} interaction + */ + given(providerState: string) { + if (providerState) { + this.state['providerState'] = providerState; + } + return this; + } + + /** + * A free style description of the interaction. + * @param {string} description - A description of the interaction. + * @returns {Interaction} interaction + */ + uponReceiving(description: string) { + if (isNil(description)) { + throw new Error('You must provide a description for the interaction.'); + } + this.state['description'] = description; + + return this; + } + + /** + * The request that represents this interaction triggered by the consumer. + * @param {Object} requestOpts + * @param {string} requestOpts.method - The HTTP method + * @param {string} requestOpts.path - The path of the URL + * @param {string} requestOpts.query - Any query string in the interaction + * @param {Object} requestOpts.headers - A key-value pair oject of headers + * @param {Object} requestOpts.body - The body, in {@link String} format or {@link Object} format + * @returns {Interaction} interaction + */ + withRequest(requestOpts: RequestOptions) { + if (isNil(requestOpts.method)) { + throw new Error('You must provide a HTTP method.') + } + + // if (VALID_METHODS.indexOf(requestOpts.method.toUpperCase()) < 0) { + // throw new Error('You must provide a valid HTTP method.'); + // } + + if (isNil(requestOpts.path)) { + throw new Error('You must provide a path.'); + } + + this.state['request'] = requestOpts; + + // omitBy({ + // method: requestOpts.method.toUpperCase(), + // path: requestOpts.path, + // query: requestOpts.query, + // headers: requestOpts.headers, + // body: requestOpts.body + // }, isNil) + + return this; + } + + /** + * The response expected by the consumer. + * @param {Object} responseOpts + * @param {string} responseOpts.status - The HTTP status + * @param {string} responseOpts.headers + * @param {Object} responseOpts.body + */ + willRespondWith(responseOpts: ResponseOptions) { + if (isNil(status) || status.toString().trim().length === 0) { + throw new Error('You must provide a status code.'); + } + + this.state['response'] = responseOpts; + // omitBy({ + // status: status, + // headers: headers || undefined, + // body: body || undefined + // }, isNil); + } + + /** + * Returns the interaction object created. + * @returns {Object} + */ + json() { + return this.state; + } +} diff --git a/src/dsl/matchers.d.ts b/src/dsl/matchers.d.ts deleted file mode 100644 index 9b70d4d8f..000000000 --- a/src/dsl/matchers.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface MatcherResult { - json_class: string; -} - -export function term(opts: {generate: string, matcher: string}): { - json_class: 'Pact::Term', - data: { - generate: string, - matcher: { - json_class: 'Regexp', - o: 0, - s: string, - }, - }, -}; - -export function eachLike(content: T, opts?: {min: number}): { - json_class: 'Pact::ArrayLike', - contents: T, - min: number, -}; - -export function somethingLike(value: T): { - json_class: 'Pact::SomethingLike', - contents: T, -}; diff --git a/src/dsl/matchers.js b/src/dsl/matchers.ts similarity index 70% rename from src/dsl/matchers.js rename to src/dsl/matchers.ts index 20db3ca70..a2702f1bd 100644 --- a/src/dsl/matchers.js +++ b/src/dsl/matchers.ts @@ -1,10 +1,6 @@ /** @module Matchers */ -'use strict' - -const isNil = require('lodash.isnil') -const isFunction = require('lodash.isfunction') -const isUndefined = require('lodash.isundefined') +import { isNil, isFunction, isUndefined } from 'lodash'; /** * The term matcher. @@ -12,12 +8,12 @@ const isUndefined = require('lodash.isundefined') * @param {string} opts.generate - a value to represent the matched String * @param {string} opts.matcher - a Regex representing the value */ -module.exports.term = (opts) => { - var generate = opts.generate - var matcher = opts.matcher +export function term(opts: { generate: string, matcher: string }) { + const generate = opts.generate; + const matcher = opts.matcher; if (isNil(generate) || isNil(matcher)) { - throw new Error('Error creating a Pact Term. Please provide an object containing "generate" and "matcher" properties') + throw new Error('Error creating a Pact Term. Please provide an object containing "generate" and "matcher" properties'); } return { @@ -30,7 +26,7 @@ module.exports.term = (opts) => { 's': matcher } } - } + }; } /** @@ -39,33 +35,37 @@ module.exports.term = (opts) => { * @param {Object} opts * @param {Number} opts.min */ -module.exports.eachLike = (content, opts) => { +export function eachLike(content: T, opts?: { min: number }) { if (isUndefined(content)) { - throw new Error('Error creating a Pact eachLike. Please provide a content argument') + throw new Error('Error creating a Pact eachLike. Please provide a content argument'); } if (opts && (isNil(opts.min) || opts.min < 1)) { - throw new Error('Error creating a Pact eachLike. Please provide opts.min that is > 1') + throw new Error('Error creating a Pact eachLike. Please provide opts.min that is > 1'); } return { 'json_class': 'Pact::ArrayLike', 'contents': content, 'min': isUndefined(opts) ? 1 : opts.min - } + }; } /** * The somethingLike matcher * @param {any} value - the value to be somethingLike */ -module.exports.somethingLike = (value) => { +export function somethingLike(value: T) { if (isNil(value) || isFunction(value)) { - throw new Error('Error creating a Pact somethingLike Match. Value cannot be a function or undefined') + throw new Error('Error creating a Pact somethingLike Match. Value cannot be a function or undefined'); } return { 'json_class': 'Pact::SomethingLike', 'contents': value - } + }; +} + +export interface MatcherResult { + json_class: string; } diff --git a/src/dsl/mockService.d.ts b/src/dsl/mockService.d.ts deleted file mode 100644 index 6048c152a..000000000 --- a/src/dsl/mockService.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Interaction } from './interaction'; - -export type PactfileWriteMode = 'overwrite' | 'update' | 'none'; - -export class MockService { - constructor( - consumer: string, - provider: string, - port?: number, - host?: string, - ssl?: boolean, - pactfileWriteMode?: PactfileWriteMode, - ); - addInteraction(interaction: Interaction): Promise; - removeInteractions(): Promise; - verify(): Promise; - writePact(): Promise; -} diff --git a/src/dsl/mockService.js b/src/dsl/mockService.js deleted file mode 100644 index 4223a6861..000000000 --- a/src/dsl/mockService.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Mock Service is the HTTP interface to setup the Pact Mock Service. - * See https://github.com/bethesque/pact-mock_service and - * https://gist.github.com/bethesque/9d81f21d6f77650811f4. - * @module MockService - */ - -'use strict' - -const isNil = require('lodash.isnil') -const Request = require('../common/request') - -module.exports = class MockService { - - /** - * @param {string} consumer - the consumer name - * @param {string} provider - the provider name - * @param {number} port - the mock service port, defaults to 1234 - * @param {string} host - the mock service host, defaults to 127.0.0.1 - * @param {boolean} ssl - which protocol to use, defaults to false (HTTP) - * @param {string} pactfileWriteMode - 'overwrite' | 'update' | 'none', defaults to 'overwrite' - */ - constructor (consumer, provider, port, host, ssl, pactfileWriteMode) { - if (isNil(consumer) || isNil(provider)) { - throw new Error('Please provide the names of the provider and consumer for this Pact.') - } - - port = port || 1234 - host = host || '127.0.0.1' - ssl = ssl || false - pactfileWriteMode = pactfileWriteMode || 'overwrite' - - this._request = new Request() - this._baseURL = `${ssl ? 'https' : 'http'}://${host}:${port}` - this._pactDetails = { - consumer: { name: consumer }, - provider: { name: provider }, - pactfile_write_mode: pactfileWriteMode - } - } - - /** - * Adds an interaction - * @param {Interaction} interaction - * @returns {Promise} - */ - addInteraction (interaction) { - const stringifiedInteraction = JSON.stringify(interaction.json()) - return this._request.send('POST', `${this._baseURL}/interactions`, stringifiedInteraction) - } - - /** - * Removes all interactions. - * @returns {Promise} - */ - removeInteractions () { - return this._request.send('DELETE', `${this._baseURL}/interactions`) - } - - /** - * Verify all interactions. - * @returns {Promise} - */ - verify () { - return this._request.send('GET', `${this._baseURL}/interactions/verification`) - } - - /** - * Writes the Pact file. - * @returns {Promise} - */ - writePact () { - const stringifiedPactDetails = JSON.stringify(this._pactDetails) - return this._request.send('POST', `${this._baseURL}/pact`, stringifiedPactDetails) - } - -} diff --git a/src/dsl/mockService.ts b/src/dsl/mockService.ts new file mode 100644 index 000000000..c409b34d8 --- /dev/null +++ b/src/dsl/mockService.ts @@ -0,0 +1,85 @@ +/** + * Mock Service is the HTTP interface to setup the Pact Mock Service. + * See https://github.com/bethesque/pact-mock_service and + * https://gist.github.com/bethesque/9d81f21d6f77650811f4. + * @module MockService + */ +import { isNil } from 'lodash'; +import { Request } from '../common/request'; +import { Interaction } from './interaction'; + +export type PactfileWriteMode = 'overwrite' | 'update' | 'none'; + +export interface PactDetails { + consumer: { name: string }; + provider: { name: string }; + pactfile_write_mode: PactfileWriteMode; +} + +export class MockService { + private pactDetails: PactDetails; + private request: Request; + private baseUrl: string; + + /** + * @param {string} consumer - the consumer name + * @param {string} provider - the provider name + * @param {number} port - the mock service port, defaults to 1234 + * @param {string} host - the mock service host, defaults to 127.0.0.1 + * @param {boolean} ssl - which protocol to use, defaults to false (HTTP) + * @param {string} pactfileWriteMode - 'overwrite' | 'update' | 'none', defaults to 'overwrite' + */ + constructor(private consumer: string, + private provider: string, + private port = 1234, + private host = '127.0.0.1', + private ssl = false, + private pactfileWriteMode: PactfileWriteMode = 'overwrite') { + + if (isNil(consumer) || isNil(provider)) { + throw new Error('Please provide the names of the provider and consumer for this Pact.') + } + + this.request = new Request(); + this.baseUrl = `${ssl ? 'https' : 'http'}://${host}:${port}`; + this.pactDetails = { + consumer: { name: consumer }, + provider: { name: provider }, + pactfile_write_mode: pactfileWriteMode + }; + } + + /** + * Adds an interaction + * @param {Interaction} interaction + * @returns {Promise} + */ + addInteraction(interaction: Interaction) { + return this.request.send('POST', `${this.baseUrl}/interactions`, JSON.stringify(interaction.json())); + } + + /** + * Removes all interactions. + * @returns {Promise} + */ + removeInteractions() { + return this.request.send('DELETE', `${this.baseUrl}/interactions`); + } + + /** + * Verify all interactions. + * @returns {Promise} + */ + verify() { + return this.request.send('GET', `${this.baseUrl}/interactions/verification`); + } + + /** + * Writes the Pact file. + * @returns {Promise} + */ + writePact() { + return this.request.send('POST', `${this.baseUrl}/pact`, JSON.stringify(this.pactDetails)); + } + +} diff --git a/src/dsl/verifier.d.ts b/src/dsl/verifier.d.ts deleted file mode 100644 index ec33d358d..000000000 --- a/src/dsl/verifier.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare function verifyProvider(opts: any): Promise; -export = verifyProvider; diff --git a/src/dsl/verifier.js b/src/dsl/verifier.js deleted file mode 100644 index c8a630318..000000000 --- a/src/dsl/verifier.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Provider Verifier service - * @module ProviderVerifier - */ - -'use strict' - -const serviceFactory = require('@pact-foundation/pact-node') - -module.exports.verifyProvider = (opts) => { - return serviceFactory.verifyPacts(opts) -} diff --git a/src/dsl/verifier.ts b/src/dsl/verifier.ts new file mode 100644 index 000000000..882082f9e --- /dev/null +++ b/src/dsl/verifier.ts @@ -0,0 +1,9 @@ +/** + * Provider Verifier service + * @module ProviderVerifier + */ +import * as serviceFactory from '@pact-foundation/pact-node'; + +export function verifyProvider(opts: any): Promise { + return serviceFactory.verifyPacts(opts); +} diff --git a/src/pact.d.ts b/src/pact.d.ts deleted file mode 100644 index 45c1cccc0..000000000 --- a/src/pact.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {InteractionObject} from './dsl/interaction'; -import * as _Matchers from './dsl/matchers'; -import {PactfileWriteMode} from './dsl/mockService'; -import _Verifier = require('./dsl/verifier'); - -declare function pact(opts: pact.PactOptions): pact.PactProvider; - -declare namespace pact { - export interface PactOptions { - consumer: string; - provider: string; - port?: number; - host?: string; - ssl?: boolean; - sslcert?: string; - sslkey?: string; - dir?: string; - log?: string; - logLevel?: string; - spec?: number; - cors?: boolean; - pactfileWriteMode?: PactfileWriteMode; - } - - export interface PactProvider { - setup(): Promise; - addInteraction(interactionObj: InteractionObject): Promise; - verify(): Promise; - finalize(): Promise; - writePact(): Promise; - removeInteractions(): Promise; - } - - export const Matchers: typeof _Matchers; - export const Verifier: typeof _Verifier; -} - -export = pact; diff --git a/src/pact.js b/src/pact.js deleted file mode 100644 index 3b79ff9ca..000000000 --- a/src/pact.js +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Pact module. - * @module Pact - */ - -'use strict' - -require('es6-promise').polyfill() - -const isNil = require('lodash.isnil') -const logger = require('./common/logger') -const net = require('./common/net') -const Matchers = require('./dsl/matchers') -const Verifier = require('./dsl/verifier') -const MockService = require('./dsl/mockService') -const Interaction = require('./dsl/interaction') -const serviceFactory = require('@pact-foundation/pact-node') -const clc = require('cli-color') -const path = require('path') - -/** - * Creates a new {@link PactProvider}. - * @memberof Pact - * @name create - * @param {Object} opts - * @param {string} opts.consumer - the name of the consumer - * @param {string} opts.provider - the name of the provider - * @param {number} opts.port - port of the mock service, defaults to 1234 - * @param {string} opts.host - host address of the mock service, defaults to 127.0.0.1 - * @param {boolean} opts.ssl - SSL flag to identify the protocol to be used (default false, HTTP) - * @param {boolean} opts.cors - allow CORS OPTION requests to be accepted, defaults to false - * @param {string} pactfileWriteMode - 'overwrite' | 'update', 'none', defaults to 'overwrite' - * @return {@link PactProvider} - * @static - */ -module.exports = (opts) => { - const consumer = opts.consumer - const provider = opts.provider - - if (isNil(consumer)) { - throw new Error('You must specify a Consumer for this pact.') - } - - if (isNil(provider)) { - throw new Error('You must specify a Provider for this pact.') - } - - const port = opts.port || 1234 - const host = opts.host || '127.0.0.1' - const ssl = opts.ssl || false - const sslcert = opts.sslcert || false - const sslkey = opts.sslkey || false - const dir = opts.dir || path.resolve(process.cwd(), 'pacts') - const log = opts.log || path.resolve(process.cwd(), 'logs', 'pact.log') - const logLevel = opts.logLevel || 'INFO' - const spec = opts.spec || 2 - const cors = opts.cors || false - const pactfileWriteMode = opts.pactfileWriteMode || 'overwrite' - - const server = serviceFactory.createServer({ - port: port, - log: log, - dir: dir, - spec: spec, - ssl: ssl, - sslcert: sslcert, - sslkey: sslkey, - cors: cors - }) - serviceFactory.logLevel(logLevel) - - logger.info(`Setting up Pact with Consumer "${consumer}" and Provider "${provider}" using mock service on Port: "${port}"`) - - const mockService = new MockService(consumer, provider, port, host, ssl, pactfileWriteMode) - - /** @namespace PactProvider */ - return { - - /** - * Start the Mock Server. - * @returns {Promise} - */ - setup: () => net.isPortAvailable(port, host).then(() => server.start()), - - /** - * Add an interaction to the {@link MockService}. - * @memberof PactProvider - * @instance - * @param {Interaction} interactionObj - * @returns {Promise} - */ - addInteraction: (interactionObj) => { - let interaction = new Interaction() - - if (interactionObj.state) { - interaction.given(interactionObj.state) - } - - interaction - .uponReceiving(interactionObj.uponReceiving) - .withRequest(interactionObj.withRequest) - .willRespondWith(interactionObj.willRespondWith) - - return mockService.addInteraction(interaction) - }, - - /** - * Checks with the Mock Service if the expected interactions have been exercised. - * @memberof PactProvider - * @instance - * @returns {Promise} - */ - verify: () => { - return mockService.verify() - .then(() => mockService.removeInteractions()) - .catch(e => { - // Properly format the error - console.error('') - console.error(clc.red('Pact verification failed!')) - console.error(clc.red(e)) - - throw new Error('Pact verification failed - expected interactions did not match actual.') - }) - }, - - /** - * Writes the Pact and clears any interactions left behind and shutdown the - * mock server - * @memberof PactProvider - * @instance - * @returns {Promise} - */ - finalize: () => mockService.writePact().then(() => server.delete()), - - /** - * Writes the pact file out to file. Should be called when all tests have been performed for a - * given Consumer <-> Provider pair. It will write out the Pact to the - * configured file. - * @memberof PactProvider - * @instance - * @returns {Promise} - */ - writePact: () => mockService.writePact(), - - /** - * Clear up any interactions in the Provider Mock Server. - * @memberof PactProvider - * @instance - * @returns {Promise} - */ - removeInteractions: () => mockService.removeInteractions() - } -} - -/** - * Exposes {@link Verifier} - * @memberof Pact - * @static - */ -module.exports.Verifier = Verifier - -/** - * Exposes {@link Matchers#term} - * @memberof Pact - * @static - */ -module.exports.Matchers = Matchers diff --git a/src/pact.ts b/src/pact.ts new file mode 100644 index 000000000..6db4aa513 --- /dev/null +++ b/src/pact.ts @@ -0,0 +1,212 @@ +/** + * Pact module. + * @module Pact + */ + +import { isNil } from 'lodash'; +import { isPortAvailable } from './common/net'; +import { MockService, PactfileWriteMode } from './dsl/mockService'; +import { Interaction, InteractionObject } from './dsl/interaction'; +import * as serviceFactory from '@pact-foundation/pact-node'; +import * as path from 'path'; +import * as process from 'process'; +import * as Matchers from './dsl/matchers'; +import * as Verifier from './dsl/verifier'; +import * as clc from 'cli-color'; +import * as logger from './common/logger'; + +// TODO: is this still needed if TypeScript is transpiling down? +// import { polyfill } from 'es6-promise'; +// polyfill(); + +// TODO: alias type for Pact for backwards compatibility? +// Add deprecation notice? + +/** + * Creates a new {@link PactProvider}. + * @memberof Pact + * @name create + * @param {PactOptions} opts + * @return {@link PactProvider} + * @static + */ +export class Pact { + private mockService: MockService; + private server: any; + private opts: PactOptionsComplete; + + constructor(config: PactOptions) { + const defaults = { + consumer: '', + provider: '', + port: 1234, + host: '127.0.0.1', + ssl: false, + dir: path.resolve(process.cwd(), 'pacts'), + log: path.resolve(process.cwd(), 'logs', 'pact.log'), + logLevel: 'INFO', + spec: 2, + cors: false, + pactfileWriteMode: 'overwrite' + } as PactOptions; + + this.opts = { ...defaults, config } as PactOptionsComplete; + + if (isNil(this.opts.consumer)) { + throw new Error('You must specify a Consumer for this pact.'); + } + + if (isNil(this.opts.provider)) { + throw new Error('You must specify a Provider for this pact.'); + } + + this.server = serviceFactory.createServer({ + port: this.opts.port, + log: this.opts.log, + dir: this.opts.dir, + spec: this.opts.spec, + ssl: this.opts.ssl, + sslcert: this.opts.sslcert, + sslkey: this.opts.sslkey, + cors: this.opts.cors + }); + serviceFactory.logLevel(this.opts.logLevel); + + logger.info(`Setting up Pact with Consumer "${this.opts.consumer}" and Provider "${this.opts.provider}" + using mock service on Port: "${this.opts.port}"`) + + this.mockService = new MockService(this.opts.consumer, this.opts.provider, this.opts.port, this.opts.host, + this.opts.ssl, this.opts.pactfileWriteMode); + } + + /** + * Start the Mock Server. + * @returns {Promise} + */ + setup(): Promise { + return isPortAvailable(this.opts.port, this.opts.host).then(() => this.server.start()); + } + + /** + * Add an interaction to the {@link MockService}. + * @memberof PactProvider + * @instance + * @param {Interaction} interactionObj + * @returns {Promise} + */ + addInteraction(interactionObj: InteractionObject): Promise { + const interaction = new Interaction(); + + if (interactionObj.state) { + interaction.given(interactionObj.state); + } + + interaction + .uponReceiving(interactionObj.uponReceiving) + .withRequest(interactionObj.withRequest) + .willRespondWith(interactionObj.willRespondWith); + + return this.mockService.addInteraction(interaction); + } + + /** + * Checks with the Mock Service if the expected interactions have been exercised. + * @memberof PactProvider + * @instance + * @returns {Promise} + */ + verify(): Promise { + return this.mockService.verify() + .then(() => this.mockService.removeInteractions()) + .catch((e: any) => { + // Properly format the error + console.error('') + console.error(clc.red('Pact verification failed!')) + console.error(clc.red(e)) + + throw new Error('Pact verification failed - expected interactions did not match actual.') + }) + } + + /** + * Writes the Pact and clears any interactions left behind and shutdown the + * mock server + * @memberof PactProvider + * @instance + * @returns {Promise} + */ + finalize(): Promise { + return this.mockService.writePact().then(() => this.server.delete()); + } + + /** + * Writes the pact file out to file. Should be called when all tests have been performed for a + * given Consumer <-> Provider pair. It will write out the Pact to the + * configured file. + * @memberof PactProvider + * @instance + * @returns {Promise} + */ + writePact(): Promise { + return this.mockService.writePact(); + } + + /** + * Clear up any interactions in the Provider Mock Server. + * @memberof PactProvider + * @instance + * @returns {Promise} + */ + removeInteractions(): Promise { + return this.mockService.removeInteractions(); + } +} + +// declare namespace pact { + +/** + * @param {string} opts.consumer - the name of the consumer + * @param {string} opts.provider - the name of the provider + * @param {number} opts.port - port of the mock service, defaults to 1234 + * @param {string} opts.host - host address of the mock service, defaults to 127.0.0.1 + * @param {boolean} opts.ssl - SSL flag to identify the protocol to be used (default false, HTTP) + * @param {boolean} opts.cors - allow CORS OPTION requests to be accepted, defaults to false + * @param {string} pactfileWriteMode - 'overwrite' | 'update', 'none', defaults to 'overwrite' + */ +export interface PactOptions { + consumer: string; + provider: string; + port?: number; + host?: string; + ssl?: boolean; + sslcert?: string; + sslkey?: string; + dir?: string; + log?: string; + logLevel?: string; + spec?: number; + cors?: boolean; + pactfileWriteMode?: PactfileWriteMode; +} + +export interface MandatoryPactOptions { + port: number; + host: string; + ssl: boolean; +} + +export type PactOptionsComplete = PactOptions & MandatoryPactOptions; + +/** + * Exposes {@link Verifier} + * @memberof Pact + * @static + */ +// export interface Verifier; + +/** + * Exposes {@link Matchers#term} + * @memberof Pact + * @static + */ +export { term, eachLike, somethingLike, somethingLike as like } from './dsl/matchers'; diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts new file mode 100644 index 000000000..6df094201 --- /dev/null +++ b/src/types/custom.d.ts @@ -0,0 +1,3 @@ +declare module '@pact-foundation/pact-node'; +declare module 'path'; +declare module 'process'; diff --git a/test/mocha.opts b/test/mocha.opts index 441b8cf3d..104c18d7d 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,5 @@ --bail +-r ts-node/register --require ./test/helper.js --reporter spec --colors diff --git a/test/pact.spec.ts b/test/pact.spec.ts new file mode 100644 index 000000000..e5c90861a --- /dev/null +++ b/test/pact.spec.ts @@ -0,0 +1,106 @@ +'use strict' +import { Pact, PactOptions } from '../src/pact'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; + +describe('Pact', () => { + const defaults = { + consumer: 'test consumer', + provider: 'provider' + } as PactOptions; + + describe('#constructor', () => { + let mockServiceMock: sinon.SinonMock; + let pact: Pact; + // let pactServer: any; //Pact; + + beforeEach(() => { + mockServiceMock = sinon.mock(Pact); + }) + + afterEach(() => { + mockServiceMock.restore(); + }) + + it('throws Error when consumer not provided', () => { + expect('foo').to.eql('foo'); + pact = new Pact(defaults); + expect(typeof pact).to.be('Pact'); + // expect(() => { PactServer({}) }).to.throw(Error, 'You must specify a Consumer for this pact.') + }) + + // it('throws Error when provider not specified', () => { + // expect(() => { Pact({ consumer: 'abc' }) }).to.throw(Error, 'You must specify a Provider for this pact.') + // }) + + // it('returns object with three functions to be invoked', (done) => { + // let pact = Pact({ consumer: 'A', provider: 'B' }) + // expect(pact).to.have.property('addInteraction') + // expect(pact).to.have.property('verify') + // expect(pact).to.have.property('finalize') + // expect(mockServiceSpy).to.have.been.calledWithNew + // expect(mockServiceSpy).to.have.been.calledWith('A', 'B', 1234, '127.0.0.1', false) + // done() + // }) + + // it('creates mockService with custom ip and port', (done) => { + // let pact = Pact({ consumer: 'A', provider: 'B', host: '192.168.10.1', port: 8443, ssl: true }) + // expect(pact).to.have.property('addInteraction') + // expect(pact).to.have.property('verify') + // expect(pact).to.have.property('finalize') + // expect(mockServiceSpy).to.have.been.calledWithNew + // expect(mockServiceSpy).to.have.been.calledWith('A', 'B', 8443, '192.168.10.1', true) + // done() + // }) + + // }) + + // describe('#addInteraction', () => { + // let pact, Pact + // let port = 4567 + + // beforeEach(() => { + // Pact = proxyquire('../src/pact', { + // './dsl/mockService': function () { + // return { addInteraction: (int) => Promise.resolve(int.json()) } + // } + // }) + // pact = Pact({ consumer: 'A', provider: 'B', port: port++ }) + // }) + + // it('creates interaction with state', (done) => { + // let addInteractionPromise = pact.addInteraction({ + // state: 'i have a list of projects', + // uponReceiving: 'a request for projects', + // withRequest: { + // method: 'get', + // path: '/projects', + // headers: { 'Accept': 'application/json' } + // }, + // willRespondWith: { + // status: 200, + // headers: { 'Content-Type': 'application/json' }, + // body: {} + // } + // }) + // expect(addInteractionPromise).to.eventually.have.property('providerState').notify(done) + // }) + + // it('creates interaction without state', (done) => { + // let addInteractionPromise = pact.addInteraction({ + // uponReceiving: 'a request for projects', + // withRequest: { + // method: 'get', + // path: '/projects', + // headers: { 'Accept': 'application/json' } + // }, + // willRespondWith: { + // status: 200, + // headers: { 'Content-Type': 'application/json' }, + // body: {} + // } + // }) + // expect(addInteractionPromise).to.eventually.not.have.property('providerState').notify(done) + // }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 36a1c0a0b..15a80286f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,9 @@ "sourceMap": true, "declaration": false, "noImplicitReturns": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, "moduleResolution": "node", "noEmitOnError": true, "emitDecoratorMetadata": true,