From 63a661bd67b1c79fc5c2791ebf6b8bd74aba6ea5 Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Thu, 21 Feb 2019 10:18:11 +1100 Subject: [PATCH] feat(proxy): cleanup verifier interface - Update TS version - Cleanup of Verifier interface - Add tests for Verifier - Move the HTTP Pact related code into it's own module - Main pact interface is now purely interface --- .travis.yml | 59 +++--- examples/e2e/test/provider.spec.js | 2 +- package-lock.json | 96 ++++----- package.json | 4 +- src/dsl/message.ts | 35 +++- src/dsl/options.ts | 1 + src/dsl/verifier.spec.ts | 153 +++++++++++++++ src/dsl/verifier.ts | 95 +++++---- src/{pact.spec.ts => httpPact.spec.ts} | 2 +- src/httpPact.ts | 237 ++++++++++++++++++++++ src/messageConsumerPact.spec.ts | 2 +- src/messageConsumerPact.ts | 4 +- src/messageProviderPact.ts | 11 +- src/pact.ts | 261 +++---------------------- 14 files changed, 602 insertions(+), 360 deletions(-) create mode 100644 src/dsl/verifier.spec.ts rename src/{pact.spec.ts => httpPact.spec.ts} (99%) create mode 100644 src/httpPact.ts diff --git a/.travis.yml b/.travis.yml index 8fc4b251a..67aaa472e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,42 +1,41 @@ language: node_js node_js: -- '6' -- '7' -- '8' -- '9' -- '10' + - "6" + - "7" + - "8" + - "9" + - "10" os: -- linux -# - osx + - linux matrix: fast_finish: true env: matrix: - - CXX=g++-4.8 + - CXX=g++-4.8 global: - # travis encrypt NPM_KEY= - - secure: "dadftjIY7w+KRucMDOqflXYTvWI1WYvtLtvGnbJuoFrgviax5zhwI7rSi6PTvW7mqV05FtqfVVb8Mgkwp1GNhv9byzuPCKnX6zN/tnl3mC++ivMA3BI7KhNp6VinzVeHWN+9BSYAQs1RQbslIK6IJ3oQJ9azp1MnxMQ1s0w5hqo0ojPWRJsm/IN57/pSiR4U0yyvONdwVg7Q8RQmyMZtovA2QzrR3ij6IxBwiJ7RQXsWIYkPL1SzaIqNNhMOdXK3m1iCESmtNc1BG9oEoaZc0ZzowT/O5VVPWe+bUfdSaAHjkTauaMCU2OAk6J89yd7pSCT5fe1YYYPgTIZiPkG0wQH8k7dKqqeaxBo+tN7uCfkYlTMNZmjv+qVBafoP8wBV97g3UugDqqIXaFknTUDnSNaigcJjFRWhCHBtltR+hzF6pCl3H1o1dDnmJWrgEb01qJ0lZonmaK/anZGNpUWE6qndOKBwnd0XiR1LnvzL/7tdflNb4DPy+lWdDEj4HWZR3lFA009m651qHBN+117ousZFXJ1866JywkAM2GrEWD4umzKknXDhulMG/Q32DS01BgW1pMenzQkH5WE+O0T3W/8BPw0Ev//bqZIg0gDckppUexHZ+pMhAFMaJfCzYVhrA0fhwLb+1EW7VDEcQIc0QHGXOb3Vclja7qB4yDuAJxk=" + # travis encrypt NPM_KEY= + - secure: "dadftjIY7w+KRucMDOqflXYTvWI1WYvtLtvGnbJuoFrgviax5zhwI7rSi6PTvW7mqV05FtqfVVb8Mgkwp1GNhv9byzuPCKnX6zN/tnl3mC++ivMA3BI7KhNp6VinzVeHWN+9BSYAQs1RQbslIK6IJ3oQJ9azp1MnxMQ1s0w5hqo0ojPWRJsm/IN57/pSiR4U0yyvONdwVg7Q8RQmyMZtovA2QzrR3ij6IxBwiJ7RQXsWIYkPL1SzaIqNNhMOdXK3m1iCESmtNc1BG9oEoaZc0ZzowT/O5VVPWe+bUfdSaAHjkTauaMCU2OAk6J89yd7pSCT5fe1YYYPgTIZiPkG0wQH8k7dKqqeaxBo+tN7uCfkYlTMNZmjv+qVBafoP8wBV97g3UugDqqIXaFknTUDnSNaigcJjFRWhCHBtltR+hzF6pCl3H1o1dDnmJWrgEb01qJ0lZonmaK/anZGNpUWE6qndOKBwnd0XiR1LnvzL/7tdflNb4DPy+lWdDEj4HWZR3lFA009m651qHBN+117ousZFXJ1866JywkAM2GrEWD4umzKknXDhulMG/Q32DS01BgW1pMenzQkH5WE+O0T3W/8BPw0Ev//bqZIg0gDckppUexHZ+pMhAFMaJfCzYVhrA0fhwLb+1EW7VDEcQIc0QHGXOb3Vclja7qB4yDuAJxk=" script: ./scripts/build.sh after_success: -- npm run coverage + - npm run coverage before_deploy: -- npm prune --production -- tar -czvf pactjs.tar.gz config dist src package.json README.md LICENSE -- npm run deploy:prepare + - npm prune --production + - tar -czvf pactjs.tar.gz config dist src package.json README.md LICENSE + - npm run deploy:prepare deploy: -- provider: releases - api_key: - secure: FmoLJnO8GNxyztR2P433ZCumYPrxiZdBCfVhmhTGYlXhOfVaAECm0gUVPLZuBQRUaqsef5ekg+OSIA5xbrnNoOQ9qlmnF2n+5yiwqG6o35XLA4L6lB5pL+x8xoAAgpaj9dTD184HKGdub3heQStTPRd2ll3nNRwxhfyIyBaMX3elDTH3mkV2QxNhG1RTgJe322PQrwoU2sWkghTWNr4t+h/G+oYu364xwZuxFX1hrFpAW+IEmbDSuhmCe24lMU96ntIiciRU3eBYR7s3KlktOFgMORXMRw3H/qaGmx7rKtpJ892XGRuVbw+tPB3A1jbFvOwJwzpnsWG5REu3PkZ6oiWpnX+5riN3jPyvFpWd+LLfH1KdZeBnF/anEfl+mSPdrDROOWotV3Xt5zOiEwx2j4BRbDNfa6wXzX0zK31AMf0IFmw7KZJkzcyWjNRluxTn3r2bbjNoi+gBojQuX27R3AQz5G0E0yZUk5ujmcd+85WOgNh/zVwsZLHYVQxDyULkbDTCDAulBsJLyxUFRs0JixyHCvA6srrUdpcO0NdyDfvULk9e/g/c9aD56Rk4xT4/Xa7K1fAHLjLkV6CA4H9Of96Zl2BK8r6LAlw382hO7FaZH+A3YShObEeTiZsDbfSQrFl5x8aimvc9oeYopvvQ+EdZxHvvwxHQIp/MWOybdJ4= - file: pactjs.tar.gz - skip_cleanup: true - on: - tags: true - branch: feat/message-pact - node: '8' -- provider: script - skip_cleanup: true - script: ./scripts/publish.sh - on: - tags: true - branch: feat/message-pact - node: '8' + - provider: releases + api_key: + secure: FmoLJnO8GNxyztR2P433ZCumYPrxiZdBCfVhmhTGYlXhOfVaAECm0gUVPLZuBQRUaqsef5ekg+OSIA5xbrnNoOQ9qlmnF2n+5yiwqG6o35XLA4L6lB5pL+x8xoAAgpaj9dTD184HKGdub3heQStTPRd2ll3nNRwxhfyIyBaMX3elDTH3mkV2QxNhG1RTgJe322PQrwoU2sWkghTWNr4t+h/G+oYu364xwZuxFX1hrFpAW+IEmbDSuhmCe24lMU96ntIiciRU3eBYR7s3KlktOFgMORXMRw3H/qaGmx7rKtpJ892XGRuVbw+tPB3A1jbFvOwJwzpnsWG5REu3PkZ6oiWpnX+5riN3jPyvFpWd+LLfH1KdZeBnF/anEfl+mSPdrDROOWotV3Xt5zOiEwx2j4BRbDNfa6wXzX0zK31AMf0IFmw7KZJkzcyWjNRluxTn3r2bbjNoi+gBojQuX27R3AQz5G0E0yZUk5ujmcd+85WOgNh/zVwsZLHYVQxDyULkbDTCDAulBsJLyxUFRs0JixyHCvA6srrUdpcO0NdyDfvULk9e/g/c9aD56Rk4xT4/Xa7K1fAHLjLkV6CA4H9Of96Zl2BK8r6LAlw382hO7FaZH+A3YShObEeTiZsDbfSQrFl5x8aimvc9oeYopvvQ+EdZxHvvwxHQIp/MWOybdJ4= + file: pactjs.tar.gz + skip_cleanup: true + on: + tags: true + branch: master + node: "8" + - provider: script + skip_cleanup: true + script: ./scripts/publish.sh + on: + tags: true + branch: master + node: "8" diff --git a/examples/e2e/test/provider.spec.js b/examples/e2e/test/provider.spec.js index 463ac38c1..84c5ba06d 100644 --- a/examples/e2e/test/provider.spec.js +++ b/examples/e2e/test/provider.spec.js @@ -86,7 +86,7 @@ describe("Pact Verification", () => { // customProviderHeaders: ['Authorization: basic e5e5e5e5e5e5e5'] } - return new Verifier().verifyProvider().then(output => { + return new Verifier().verifyProvider(opts).then(output => { console.log("Pact Verification Complete!") console.log(output) }) diff --git a/package-lock.json b/package-lock.json index 85541b733..195d07c16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@pact-foundation/pact", - "version": "7.3.0", + "version": "8.0.0-beta", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -398,9 +398,20 @@ } }, "@sinonjs/samsam": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.3.tgz", - "integrity": "sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.1.1.tgz", + "integrity": "sha512-ILlwvQUwAiaVBzr3qz8oT1moM7AIUHqUc2UmEjQcH9lLe+E+BZPwUMuc9FFojMswRK4r96x5zDTTrowMLw/vuA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, "@types/bluebird": { @@ -558,9 +569,9 @@ } }, "@types/sinon": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-2.3.7.tgz", - "integrity": "sha1-6Swv7TKX6uB4140doDKyZ4i0r4Y=", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.6.tgz", + "integrity": "sha512-ldQl2p7kyCXHhr//5sQCu9jWgSiDbYuCD5dUp53r/z8T8jmgKpANpy4vqjbdho9P2ldFaQC/gpW31hch+oQ0IQ==", "dev": true }, "@types/sinon-chai": { @@ -850,6 +861,12 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -3266,7 +3283,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -4282,8 +4298,7 @@ "eventemitter3": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", - "dev": true + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" }, "events": { "version": "1.1.1", @@ -4884,7 +4899,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", - "dev": true, "requires": { "debug": "=3.1.0" } @@ -6022,7 +6036,6 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "dev": true, "requires": { "eventemitter3": "^3.0.0", "follow-redirects": "^1.0.0", @@ -7863,12 +7876,6 @@ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, "lodash.isfunction": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz", @@ -7971,9 +7978,9 @@ } }, "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", + "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", "dev": true }, "longest": { @@ -8630,16 +8637,24 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "nise": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.8.tgz", - "integrity": "sha512-kGASVhuL4tlAV0tvA34yJYZIVihrUt/5bDwpp4tTluigxUr2bBlJeDXmivb6NuEdFkqvdv/Ybb9dm16PSKUhtw==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", "dev": true, "requires": { "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "lolex": "^2.3.2", - "path-to-regexp": "^1.7.0", - "text-encoding": "^0.6.4" + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } } }, "nock": { @@ -11067,8 +11082,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { "version": "1.1.7", @@ -11369,20 +11383,18 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "sinon": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz", - "integrity": "sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==", + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.4.tgz", + "integrity": "sha512-FGlcfrkiBRfaEIKRw8s/9mk4nP4AMGswvKFixLo+AzsOhskjaBCHAHGLMd8pCJpQGS+9ZI71px6qoQUyvADeyA==", "dev": true, "requires": { - "@sinonjs/commons": "^1.0.2", - "@sinonjs/formatio": "^3.0.0", - "@sinonjs/samsam": "^2.1.2", + "@sinonjs/commons": "^1.3.0", + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/samsam": "^3.1.1", "diff": "^3.5.0", - "lodash.get": "^4.4.2", - "lolex": "^2.7.5", - "nise": "^1.4.5", - "supports-color": "^5.5.0", - "type-detect": "^4.0.8" + "lolex": "^3.1.0", + "nise": "^1.4.10", + "supports-color": "^5.5.0" }, "dependencies": { "diff": { @@ -12353,12 +12365,6 @@ "xtend": "^4.0.0" } }, - "text-encoding": { - "version": "0.6.4", - "resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, "text-extensions": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.8.0.tgz", diff --git a/package.json b/package.json index 667b05f6d..f7eb58648 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@types/node": "^8.0.24", "@types/proxyquire": "^1.3.27", "@types/q": "^1.0.6", - "@types/sinon": "^2.3.3", + "@types/sinon": "^7.0.6", "@types/sinon-chai": "^2.7.29", "@types/superagent": "^3.5.7", "@types/tough-cookie": "^2.3.2", @@ -150,7 +150,7 @@ "nyc": "^13.1.0", "proxyquire": "^2.0.1", "rimraf": "^2.6.2", - "sinon": "^6.3.5", + "sinon": "^7.2.4", "sinon-chai": "^2.13.0", "source-map-loader": "^0.2.1", "source-map-support": "^0.4.18", diff --git a/src/dsl/message.ts b/src/dsl/message.ts index 3feb495a7..9dd7ba5d9 100644 --- a/src/dsl/message.ts +++ b/src/dsl/message.ts @@ -2,7 +2,7 @@ import { MatcherResult } from "./matchers" /** * Metadata is a map containing message context, - * such as content-type etc. + * such as content-type, correlation IDs etc. * * @module Message */ @@ -12,7 +12,6 @@ export interface Metadata { /** * A Message is an asynchronous Interaction, sent via a Provider - * (consumer in the http, synchronous interaction parlance) * * @module Message */ @@ -23,12 +22,40 @@ export interface Message { contents: any } -// Message producer/handlers +/** + * A Message Descriptor is a set of additional context for a given message + * + * @module Message + */ +export interface MessageDescriptor { + providerStates?: [{ name: string }] + description: string + metadata?: Metadata +} + +/** + * A Message Consumer is a function that will receive a message + * from a given Message Provider. It is given the full Message + * context during verification. + * + * @module Message + */ export type MessageConsumer = (m: Message) => Promise -export type MessageProvider = (m: Message) => Promise + +/** + * A Message Provider is a function that will be invoked by the framework + * in order to _produce_ a message for a consumer. The response must match what + * the given consumer has specified in the pact file. It is given a Message + * Descriptor object when being invoked which can be used for additional context. + * + * @module Message + */ +export type MessageProvider = (m: MessageDescriptor) => Promise + export interface MessageProviders { [name: string]: MessageProvider } + export interface StateHandlers { [name: string]: (state: string) => Promise } diff --git a/src/dsl/options.ts b/src/dsl/options.ts index 329a68535..52be21564 100644 --- a/src/dsl/options.ts +++ b/src/dsl/options.ts @@ -93,6 +93,7 @@ export interface MessageProviderOptions { tags?: string[] timeout?: number } + export interface MessageConsumerOptions { // The name of the consumer consumer: string diff --git a/src/dsl/verifier.spec.ts b/src/dsl/verifier.spec.ts new file mode 100644 index 000000000..66c592daf --- /dev/null +++ b/src/dsl/verifier.spec.ts @@ -0,0 +1,153 @@ +/* tslint:disable:no-unused-expression no-empty no-string-literal*/ +import * as chai from "chai" +import * as chaiAsPromised from "chai-as-promised" +import * as sinon from "sinon" +import { Verifier, VerifierOptions } from "./verifier" +import serviceFactory from "@pact-foundation/pact-node" +import logger from "../common/logger" + +chai.use(chaiAsPromised) + +const expect = chai.expect + +describe("Verifier", () => { + afterEach(() => { + sinon.restore() + }) + + const state = "thing exists" + let v: Verifier + let opts: VerifierOptions + let executed: boolean + const providerBaseUrl = "http://not.exists" + + beforeEach(() => { + executed = false + opts = { + providerBaseUrl, + requestFilter: (req, res, next) => { + executed = true + next() + }, + stateHandlers: { + [state]: () => { + return Promise.resolve("done") + }, + }, + } + }) + + describe("#constructor", () => { + describe("when given configuration", () => { + it("sets the configuration on the object", () => { + v = new Verifier(opts) + expect(v) + .to.have.deep.property("config") + .includes({ + providerBaseUrl, + }) + expect(v).to.have.nested.property("config.stateHandlers") + expect(v).to.have.nested.property("config.requestFilter") + }) + }) + describe("when no configuration is given", () => { + it("does not set the configuration on the object", () => { + v = new Verifier() + expect(v).to.not.have.deep.property("config") + }) + }) + }) + describe("#setConfig", () => { + let spy: sinon.SinonSpy + beforeEach(() => { + spy = sinon.spy(serviceFactory, "logLevel") + v = new Verifier(opts) + }) + context("when logLevel is provided", () => { + it("sets the log level on pact node", () => { + v["setConfig"]({ + ...opts, + logLevel: "debug", + }) + expect(spy.callCount).to.eql(1) + }) + }) + context("when logLevel is not provided", () => { + it("does not modify the log setting", () => { + v["setConfig"]({ + ...opts, + }) + expect(spy.callCount).to.eql(0) + }) + }) + }) + describe("#setupStates", () => { + describe("when there are provider states on the pact", () => { + describe("and there are handlers associated with those states", () => { + it("executes the handler and returns a set of Promises", async () => { + v = new Verifier(opts) + const res = await v["setupStates"]({ + states: [state], + }) + expect(res).lengthOf(1) + }) + }) + describe("and there are no handlers associated with those states", () => { + it("executes the handler and returns an empty Promise", async () => { + const spy = sinon.spy(logger, "warn") + delete opts.stateHandlers + v = new Verifier(opts) + const res = await v["setupStates"]({ + states: [state], + }) + expect(res).lengthOf(0) + expect(spy.callCount).to.eql(1) + }) + }) + }) + describe("when there are no provider states on the pact", () => { + it("executes the handler and returns an empty Promise", async () => { + v = new Verifier(opts) + const res = await v["setupStates"]({}) + expect(res).lengthOf(0) + }) + }) + }) + describe("#verifyProvider", () => { + // beforeEach(() => { + // v = new Verifier() + // }) + it("creates a Provider when all mandatory parameters are provided", () => {}) + + // describe("when the provider state has been given a handler", () => { + // it("executes the handler", async () => { + // const stub = sinon + // .stub() + // .returns({ promise: () => Promise.resolve({ foo: "bar" }) }) + // sinon.stub(v, "setupProxyApplication" as any).returns({}) + // sinon.stub(v, "setupProxyServer" as any).returns({ close: () => {} }) + // sinon.stub(v, "waitForServerReady" as any).returns(Promise.resolve()) + // sinon + // .stub(v, "runProviderVerification" as any) + // .returns(Promise.resolve("done")) + // await v.verifyProvider() + // expect(executed).to.eq(true) + // }) + // }) + }) + describe("#waitForServerReady", () => { + it("creates a Provider when all mandatory parameters are provided", () => {}) + }) + describe("#runProviderVerification", () => { + it("creates a Provider when all mandatory parameters are provided", () => {}) + }) + describe("#setupStateHandler", () => { + it("creates a Provider when all mandatory parameters are provided", () => {}) + }) + describe("#setupProxyServer", () => { + it("creates a Provider when all mandatory parameters are provided", () => {}) + }) + describe("#setupProxyApplicationn", () => { + it("creates a Provider when all mandatory parameters are provided", () => {}) + }) +}) diff --git a/src/dsl/verifier.ts b/src/dsl/verifier.ts index e59e953e1..64c0e844d 100644 --- a/src/dsl/verifier.ts +++ b/src/dsl/verifier.ts @@ -9,41 +9,58 @@ import serviceFactory from "@pact-foundation/pact-node" import { omit, isEmpty } from "lodash" import * as express from "express" import * as http from "http" -const HttpProxy = require("http-proxy") import logger from "../common/logger" import { LogLevel } from "./options" import ConfigurationError from "../errors/configurationError" - +const HttpProxy = require("http-proxy") const bodyParser = require("body-parser") export interface ProviderState { states?: [string] } -interface StateHandlers { - [name: string]: (state: string) => Promise +export interface StateHandlers { + stateHandlers: StateHandler } -interface StateOptions { - requestFilter?: express.RequestHandler - stateHandlers?: StateHandlers +export interface StateHandler { + [name: string]: (state: string) => Promise } -interface LogOptions { +// See https://stackoverflow.com/questions/43357734/typescript-recursive-type-with-indexer/43359686 +// as to why we can't use an intersection type here +// TL;DR - PactNodeVerifierOptions has an index type which enforces all keys to match the index type +export interface VerifierOptions { logLevel?: LogLevel + requestFilter?: express.RequestHandler + stateHandlers?: StateHandler + providerBaseUrl: string + provider?: string + pactUrls?: string[] + pactBrokerBaseUrl?: string + providerStatesSetupUrl?: string + pactBrokerUsername?: string + pactBrokerPassword?: string + consumerVersionTag?: string + customProviderHeaders?: string[] + publishVerificationResult?: boolean + providerVersion?: string + pactBrokerUrl?: string + tags?: string[] + timeout?: number + monkeypatch?: string + format?: "json" | "RspecJunitFormatter" + out?: string } -export type VerifierOptions = PactNodeVerifierOptions & - StateOptions & - LogOptions - export class Verifier { - constructor(private config: VerifierOptions) { - if (config.logLevel && !isEmpty(config.logLevel)) { - serviceFactory.logLevel(config.logLevel) - logger.level(config.logLevel) - } else { - logger.level() + private address: string = "http://localhost" + private stateSetupPath: string = "/_pactSetup" + private config: VerifierOptions + + constructor(config?: VerifierOptions) { + if (config) { + this.setConfig(config) } } @@ -55,15 +72,18 @@ export class Verifier { // Backwards compatibility if (config) { - this.config = config + logger.warn( + "Passing options to verifyProvider() wil be deprecated in future versions, please provide to Verifier constructor instead" + ) + this.setConfig(config) } - // TODO: use a real Error type here. Consider doing the same for all errors if (isEmpty(this.config)) { return Promise.reject( - new ConfigurationError("no configuration provided to verifier") + new ConfigurationError("No configuration provided to verifier") ) } + // Start the verification CLI proxy server const app = this.setupProxyApplication() const server = this.setupProxyServer(app) @@ -91,14 +111,14 @@ export class Verifier { return (server: http.Server) => { const opts = { ...omit(this.config, "handlers"), - ...{ providerBaseUrl: "http://localhost:" + server.address().port }, + ...{ providerBaseUrl: `${this.address}:${server.address().port}` }, ...{ - providerStatesSetupUrl: - "http://localhost:" + server.address().port + "/setup", + providerStatesSetupUrl: `${this.address}:${server.address().port}${ + this.stateSetupPath + }`, }, - } as VerifierOptions + } as PactNodeVerifierOptions - // Run verification return qToPromise(pact.verifyPacts(opts)) } } @@ -136,8 +156,8 @@ export class Verifier { // TODO: these should probably only go on the routes we intercept (e.g. /setup) // ...make this path configurable / dynamic or on a separate port altogether - app.use("/setup", bodyParser.json()) - app.use("/setup", bodyParser.urlencoded({ extended: true })) + app.use(this.stateSetupPath, bodyParser.json()) + app.use(this.stateSetupPath, bodyParser.urlencoded({ extended: true })) // Allow for request filtering if (this.config.requestFilter !== undefined) { @@ -147,7 +167,7 @@ export class Verifier { // TODO: Set this to simply run on a specific, pre-defined path // ...possibly even run it on a different port to avoid conflicts?? // ...make this user configurable - app.post("/setup", (req, res, next) => { + app.post(this.stateSetupPath, (req, res, next) => { const message: ProviderState = req.body // Invoke the handler, return an error if promise fails @@ -168,11 +188,11 @@ export class Verifier { } // Lookup the handler based on the description, or get the default handler - private setupStates(message: ProviderState): Promise { + private setupStates(descriptor: ProviderState): Promise { const promises: Array> = new Array() - if (message.states) { - message.states.forEach(state => { + if (descriptor.states) { + descriptor.states.forEach(state => { const handler = this.config.stateHandlers ? this.config.stateHandlers[state] : null @@ -180,11 +200,20 @@ export class Verifier { if (handler) { promises.push(handler(state)) } else { - logger.warn(`no state handler found for "${state}", ignorning`) + logger.warn(`No state handler found for "${state}", ignorning`) } }) } return Promise.all(promises) } + + private setConfig(config: VerifierOptions) { + this.config = config + + if (this.config.logLevel && !isEmpty(this.config.logLevel)) { + serviceFactory.logLevel(this.config.logLevel) + logger.level(this.config.logLevel) + } + } } diff --git a/src/pact.spec.ts b/src/httpPact.spec.ts similarity index 99% rename from src/pact.spec.ts rename to src/httpPact.spec.ts index e6f0e3fa1..d24736c12 100644 --- a/src/pact.spec.ts +++ b/src/httpPact.spec.ts @@ -7,7 +7,7 @@ import { HTTPMethod } from "./common/request" import { Interaction, InteractionObject } from "./dsl/interaction" import { MockService } from "./dsl/mockService" import { PactOptions, PactOptionsComplete } from "./dsl/options" -import { Pact as PactType } from "./pact" +import { Pact as PactType } from "./httpPact" chai.use(sinonChai) chai.use(chaiAsPromised) diff --git a/src/httpPact.ts b/src/httpPact.ts new file mode 100644 index 000000000..a4aaac5a6 --- /dev/null +++ b/src/httpPact.ts @@ -0,0 +1,237 @@ +import serviceFactory from "@pact-foundation/pact-node" +import * as path from "path" +import * as clc from "cli-color" +import * as process from "process" +import { Interaction, InteractionObject } from "./dsl/interaction" +import { isEmpty } from "lodash" +import { isPortAvailable } from "./common/net" +import logger from "./common/logger" +import { LogLevels } from "@pact-foundation/pact-node/src/logger" +import { MockService } from "./dsl/mockService" +import { PactOptions, PactOptionsComplete } from "./dsl/options" +import { Server } from "@pact-foundation/pact-node/src/server" +import VerificationError from "./errors/verificationError" +import ConfigurationError from "./errors/configurationError" + +/** + * Creates a new {@link PactProvider}. + * @memberof Pact + * @name create + * @param {PactOptions} opts + * @return {@link PactProvider} + */ +export class Pact { + public static defaults = { + consumer: "", + cors: false, + dir: path.resolve(process.cwd(), "pacts"), + host: "127.0.0.1", + log: path.resolve(process.cwd(), "logs", "pact.log"), + logLevel: "info", + pactfileWriteMode: "overwrite", + provider: "", + spec: 2, + ssl: false, + } as PactOptions + + public static createOptionsWithDefaults( + opts: PactOptions + ): PactOptionsComplete { + return { ...Pact.defaults, ...opts } as PactOptionsComplete + } + + public server: Server + public opts: PactOptionsComplete + public mockService: MockService + private finalized: boolean + + constructor(config: PactOptions) { + this.opts = Pact.createOptionsWithDefaults(config) + + if (isEmpty(this.opts.consumer)) { + throw new ConfigurationError("You must specify a Consumer for this pact.") + } + + if (isEmpty(this.opts.provider)) { + throw new ConfigurationError("You must specify a Provider for this pact.") + } + + logger.level(this.opts.logLevel as LogLevels) + serviceFactory.logLevel(this.opts.logLevel) + this.server = serviceFactory.createServer({ + consumer: this.opts.consumer, + cors: this.opts.cors, + dir: this.opts.dir, + host: this.opts.host, + log: this.opts.log, + pactFileWriteMode: this.opts.pactfileWriteMode, + port: config.port, // allow to be undefined + provider: this.opts.provider, + spec: this.opts.spec, + ssl: this.opts.ssl, + sslcert: this.opts.sslcert, + sslkey: this.opts.sslkey, + }) + } + + /** + * Setup the pact framework, including start the + * underlying mock server + * @returns {Promise} + */ + public setup(): Promise { + return this.checkPort() + .then(() => this.startServer()) + .then(opts => { + this.setupMockService() + return Promise.resolve(opts) + }) + } + + /** + * Add an interaction to the {@link MockService}. + * @memberof PactProvider + * @instance + * @param {Interaction} interactionObj + * @returns {Promise} + */ + public addInteraction( + interactionObj: InteractionObject | Interaction + ): Promise { + if (interactionObj instanceof Interaction) { + return this.mockService.addInteraction(interactionObj) + } + 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} + */ + public verify(): Promise { + return this.mockService + .verify() + .then(() => this.mockService.removeInteractions()) + .catch((e: any) => { + // Properly format the error + /* tslint:disable: no-console */ + console.error("") + console.error(clc.red("Pact verification failed!")) + console.error(clc.red(e)) + /* tslint:enable: */ + + return this.mockService.removeInteractions().then(() => { + throw new VerificationError( + "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} + */ + public finalize(): Promise { + if (this.finalized) { + logger.warn( + "finalize() has already been called, this is probably a logic error in your test setup. " + + "In the future this will be an error." + ) + } + this.finalized = true + + return this.mockService + .writePact() + .then( + () => logger.info("Pact File Written"), + e => { + return Promise.reject(e) + } + ) + .then( + () => + new Promise((resolve, reject) => + this.server.delete().then(() => resolve(), e => reject(e)) + ) + ) + .catch( + (e: Error) => + new Promise((resolve, reject) => { + return this.server.delete().finally(() => reject(e)) + }) + ) + } + + /** + * 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} + */ + public writePact(): Promise { + return this.mockService.writePact() + } + + /** + * Clear up any interactions in the Provider Mock Server. + * @memberof PactProvider + * @instance + * @returns {Promise} + */ + public removeInteractions(): Promise { + return this.mockService.removeInteractions() + } + + private checkPort(): Promise { + if (this.server && this.server.options.port) { + return isPortAvailable(this.server.options.port, this.opts.host) + } + return Promise.resolve() + } + + private setupMockService(): void { + 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( + undefined, + undefined, + this.opts.port, + this.opts.host, + this.opts.ssl, + this.opts.pactfileWriteMode + ) + } + + private startServer(): Promise { + return new Promise((resolve, reject) => + this.server.start().then( + () => { + this.opts.port = this.server.options.port || this.opts.port + resolve(this.opts) + }, + (e: any) => reject(e) + ) + ) + } +} diff --git a/src/messageConsumerPact.spec.ts b/src/messageConsumerPact.spec.ts index 490cd671a..a8bd0f6e6 100644 --- a/src/messageConsumerPact.spec.ts +++ b/src/messageConsumerPact.spec.ts @@ -6,7 +6,7 @@ import { synchronousBodyHandler, asynchronousBodyHandler, } from "./messageConsumerPact" -import { Message } from "./dsl/message" +import { Message, MessageDescriptor } from "./dsl/message" import * as sinonChai from "sinon-chai" chai.use(sinonChai) diff --git a/src/messageConsumerPact.ts b/src/messageConsumerPact.ts index 0cade8881..99e6aa1ba 100644 --- a/src/messageConsumerPact.ts +++ b/src/messageConsumerPact.ts @@ -169,7 +169,7 @@ const isMessage = (x: Message | any): x is Message => { // a wrapped function that accepts a Message and returns a Promise export function synchronousBodyHandler( handler: (body: any) => any -): MessageProvider { +): MessageConsumer { return (m: Message): Promise => { const body = m.contents @@ -189,6 +189,6 @@ export function synchronousBodyHandler( // TODO: move this into its own package and re-export? export function asynchronousBodyHandler( handler: (body: any) => Promise -): MessageProvider { +): MessageConsumer { return (m: Message) => handler(m.contents) } diff --git a/src/messageProviderPact.ts b/src/messageProviderPact.ts index b4acca523..a8a56719f 100644 --- a/src/messageProviderPact.ts +++ b/src/messageProviderPact.ts @@ -4,7 +4,7 @@ import { omit, isEmpty } from "lodash" import { Verifier } from "./dsl/verifier" -import { Message } from "./dsl/message" +import { Message, MessageDescriptor } from "./dsl/message" import logger from "./common/logger" import { VerifierOptions } from "@pact-foundation/pact-node" import { MessageProviderOptions } from "./dsl/options" @@ -67,8 +67,6 @@ export class MessageProviderPact { ...{ providerBaseUrl: "http://localhost:" + server.address().port }, } as VerifierOptions - // Run verification - // TODO: backwards incompatible change here return new Verifier(opts).verifyProvider() } } @@ -79,8 +77,7 @@ export class MessageProviderPact { res: express.Response ) => void { return (req, res) => { - // Extract the message request from the API - const message: Message = req.body + const message: MessageDescriptor = req.body // Invoke the handler, and return the JSON response body // wrapped in a Message @@ -117,7 +114,7 @@ export class MessageProviderPact { } // Lookup the handler based on the description, or get the default handler - private setupStates(message: Message): Promise { + private setupStates(message: MessageDescriptor): Promise { const promises: Array> = new Array() if (message.providerStates) { @@ -137,7 +134,7 @@ export class MessageProviderPact { return Promise.all(promises) } // Lookup the handler based on the description, or get the default handler - private findHandler(message: Message): Promise { + private findHandler(message: MessageDescriptor): Promise { const handler = this.config.messageProviders[message.description || ""] if (!handler) { diff --git a/src/pact.ts b/src/pact.ts index 06a977bcb..d0e225c30 100644 --- a/src/pact.ts +++ b/src/pact.ts @@ -1,247 +1,34 @@ /** - * Pact module. + * Pact module meta package. * @module Pact */ -import serviceFactory from "@pact-foundation/pact-node" -import * as clc from "cli-color" -import * as Matchers from "./dsl/matchers" -import * as path from "path" -import * as process from "process" -import { Interaction, InteractionObject } from "./dsl/interaction" -import { isEmpty } from "lodash" -import { isPortAvailable } from "./common/net" -import logger from "./common/logger" -import { LogLevels } from "@pact-foundation/pact-node/src/logger" -import { MockService } from "./dsl/mockService" -import { PactOptions, PactOptionsComplete } from "./dsl/options" -import { Server } from "@pact-foundation/pact-node/src/server" -import VerificationError from "./errors/verificationError" -import ConfigurationError from "./errors/configurationError" /** - * Creates a new {@link PactProvider}. + * Exposes {@link Pact} * @memberof Pact - * @name create - * @param {PactOptions} opts - * @return {@link PactProvider} + * @static */ -export class Pact { - public static defaults = { - consumer: "", - cors: false, - dir: path.resolve(process.cwd(), "pacts"), - host: "127.0.0.1", - log: path.resolve(process.cwd(), "logs", "pact.log"), - logLevel: "info", - pactfileWriteMode: "overwrite", - provider: "", - spec: 2, - ssl: false, - } as PactOptions - - public static createOptionsWithDefaults( - opts: PactOptions - ): PactOptionsComplete { - return { ...Pact.defaults, ...opts } as PactOptionsComplete - } - - public server: Server - public opts: PactOptionsComplete - public mockService: MockService - private finalized: boolean - - constructor(config: PactOptions) { - this.opts = Pact.createOptionsWithDefaults(config) - - if (isEmpty(this.opts.consumer)) { - throw new ConfigurationError("You must specify a Consumer for this pact.") - } - - if (isEmpty(this.opts.provider)) { - throw new ConfigurationError("You must specify a Provider for this pact.") - } - - logger.level(this.opts.logLevel as LogLevels) - serviceFactory.logLevel(this.opts.logLevel) - this.server = serviceFactory.createServer({ - consumer: this.opts.consumer, - cors: this.opts.cors, - dir: this.opts.dir, - host: this.opts.host, - log: this.opts.log, - pactFileWriteMode: this.opts.pactfileWriteMode, - port: config.port, // allow to be undefined - provider: this.opts.provider, - spec: this.opts.spec, - ssl: this.opts.ssl, - sslcert: this.opts.sslcert, - sslkey: this.opts.sslkey, - }) - } - - /** - * Start the Mock Server. - * @returns {Promise} - */ - public setup(): Promise { - return this.checkPort() - .then(() => this.startServer()) - .then(opts => { - this.setupMockService() - return Promise.resolve(opts) - }) - } - - /** - * Add an interaction to the {@link MockService}. - * @memberof PactProvider - * @instance - * @param {Interaction} interactionObj - * @returns {Promise} - */ - public addInteraction( - interactionObj: InteractionObject | Interaction - ): Promise { - if (interactionObj instanceof Interaction) { - return this.mockService.addInteraction(interactionObj) - } - 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} - */ - public verify(): Promise { - return this.mockService - .verify() - .then(() => this.mockService.removeInteractions()) - .catch((e: any) => { - // Properly format the error - /* tslint:disable: no-console */ - console.error("") - console.error(clc.red("Pact verification failed!")) - console.error(clc.red(e)) - /* tslint:enable: */ - - return this.mockService.removeInteractions().then(() => { - throw new VerificationError( - "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} - */ - public finalize(): Promise { - if (this.finalized) { - logger.warn( - "finalize() has already been called, this is probably a logic error in your test setup. " + - "In the future this will be an error." - ) - } - this.finalized = true - - return this.mockService - .writePact() - .then( - () => logger.info("Pact File Written"), - e => { - return Promise.reject(e) - } - ) - .then( - () => - new Promise((resolve, reject) => - this.server.delete().then(() => resolve(), e => reject(e)) - ) - ) - .catch( - (e: Error) => - new Promise((resolve, reject) => { - return this.server.delete().finally(() => reject(e)) - }) - ) - } - - /** - * 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} - */ - public writePact(): Promise { - return this.mockService.writePact() - } - - /** - * Clear up any interactions in the Provider Mock Server. - * @memberof PactProvider - * @instance - * @returns {Promise} - */ - public removeInteractions(): Promise { - return this.mockService.removeInteractions() - } - - private checkPort(): Promise { - if (this.server && this.server.options.port) { - return isPortAvailable(this.server.options.port, this.opts.host) - } - return Promise.resolve() - } - - private setupMockService(): void { - 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( - undefined, - undefined, - this.opts.port, - this.opts.host, - this.opts.ssl, - this.opts.pactfileWriteMode - ) - } - - private startServer(): Promise { - return new Promise((resolve, reject) => - this.server.start().then( - () => { - this.opts.port = this.server.options.port || this.opts.port - resolve(this.opts) - }, - (e: any) => reject(e) - ) - ) - } -} +export * from "./httpPact" +/** + * Exposes {@link MessageConsumerPact} + * @memberof Pact + * @static + */ export * from "./messageConsumerPact" + +/** + * Exposes {@link MessageProviderPact} + * @memberof Pact + * @static + */ export * from "./messageProviderPact" + +/** + * Exposes {@link Message} + * @memberof Pact + * @static + */ export * from "./dsl/message" /** @@ -257,6 +44,11 @@ export * from "./dsl/verifier" * @static */ export * from "./dsl/graphql" +/** + * Exposes {@link ApolloGraphQL} + * @memberof Pact + * @static + */ export * from "./dsl/apolloGraphql" /** @@ -266,6 +58,7 @@ export * from "./dsl/apolloGraphql" * @memberof Pact * @static */ +import * as Matchers from "./dsl/matchers" export import Matchers = Matchers /**