├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── issue_template.md ├── pact.svg ├── pull_request_template.md ├── renovate.json ├── semantic.yml ├── stale.yml └── workflows │ ├── publish-9x.yml │ ├── publish.yml │ ├── smartbear-issue-label-added.yml │ ├── test.yml │ ├── triage.yml │ └── trigger_pact_docs_update.yml ├── .gitignore ├── .istanbul.yml ├── .mocharc.json ├── .npmignore ├── .nycrc ├── .prettierignore ├── .prettierrc.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile.alpine ├── Dockerfile.debian ├── LICENSE ├── MIGRATION.md ├── README.md ├── RELEASING.md ├── appveyor.yml ├── docs ├── consumer.md ├── diagrams │ ├── message-consumer.png │ ├── message-provider.png │ ├── summary.png │ ├── workshop_step1.svg │ ├── workshop_step10_broker.svg │ ├── workshop_step1_class-sequence-diagram.svg │ ├── workshop_step1_failed_page.png │ ├── workshop_step2_failed_page.png │ ├── workshop_step2_unit_test.svg │ ├── workshop_step3_pact.svg │ ├── workshop_step4_pact.svg │ └── workshop_step5_pact.svg ├── graphql.md ├── matching.md ├── messages.md ├── migrations │ └── 9-10.md ├── plugins.md ├── provider.md ├── troubleshooting.md └── xml.md ├── examples ├── e2e │ ├── .editorconfig │ ├── .mocharc.json │ ├── README.md │ ├── consumer.js │ ├── consumerService.js │ ├── data │ │ └── animalData.json │ ├── package-lock.json │ ├── package.json │ ├── pact-js-e2e.postman_collection │ ├── provider.js │ ├── providerService.js │ ├── repository.js │ └── test │ │ ├── consumer.spec.js │ │ └── provider.spec.js ├── graphql │ ├── .gitignore │ ├── .mocharc.json │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── consumer.spec.ts │ │ ├── consumer.ts │ │ ├── provider.spec.ts │ │ └── provider.ts │ ├── tsconfig.json │ └── tslint.json ├── jest │ ├── .mocharc.json │ ├── README.md │ ├── __tests__ │ │ └── index.spec.js │ ├── index.js │ ├── package-lock.json │ └── package.json ├── messages │ ├── .gitignore │ ├── .mocharc.json │ ├── README.md │ ├── consumer │ │ ├── dog-handler.ts │ │ └── message-consumer.spec.ts │ ├── package-lock.json │ ├── package.json │ ├── provider │ │ ├── dog-client.ts │ │ ├── message-provider.spec.ts │ │ └── msg-client.ts │ ├── tsconfig.json │ └── tslint.json ├── mocha │ ├── .mocharc.json │ ├── README.md │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── test │ │ └── get-dogs.spec.js ├── run-all ├── serverless │ ├── .gitignore │ ├── .mocharc.json │ ├── README.md │ ├── consumer │ │ ├── consumer.spec.js │ │ └── index.js │ ├── package-lock.json │ ├── package.json │ ├── provider │ │ ├── .gitignore │ │ ├── index.js │ │ └── message-provider.spec.js │ └── serverless.yml ├── typescript │ ├── .mocharc.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ └── get-dog.spec.ts │ ├── tsconfig.json │ └── tslint.json ├── v3 │ ├── e2e │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── .mocharc.json │ │ ├── README.md │ │ ├── Vagrantfile │ │ ├── consumer.js │ │ ├── consumerService.js │ │ ├── data │ │ │ ├── animalData.json │ │ │ └── data.xml │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── pact-js-e2e.postman_collection │ │ ├── provider.js │ │ ├── providerService.js │ │ ├── repository.js │ │ └── test │ │ │ ├── consumer.spec.js │ │ │ └── provider.spec.js │ ├── provider-state-injected │ │ ├── README.md │ │ ├── consumer │ │ │ ├── transaction-service.js │ │ │ └── transaction-service.test.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── provider │ │ │ ├── account-repository.js │ │ │ ├── account-service.js │ │ │ └── account-service.test.js │ ├── run-specific-verifications │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── .mocharc.json │ │ ├── README.md │ │ ├── filter-by-PACT_DESCRIPTION.json │ │ ├── filter-by-PACT_PROVIDER_NO_STATE.json │ │ ├── filter-by-PACT_PROVIDER_STATE.json │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── provider.js │ │ └── test │ │ │ └── provider.spec.js │ ├── run.sh │ ├── todo-consumer │ │ ├── .mocharc.json │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── provider.js │ │ ├── providerService.js │ │ ├── src │ │ │ └── todo.js │ │ └── test │ │ │ ├── consumer.spec.js │ │ │ ├── example.jpg │ │ │ └── provider.spec.js │ └── typescript │ │ ├── .mocharc.json │ │ ├── index.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── test │ │ └── user.spec.ts │ │ ├── tsconfig.json │ │ └── tslint.json └── v4 │ ├── matchers │ ├── .mocharc.json │ ├── consumer.spec.ts │ ├── package-lock.json │ ├── package.json │ └── provider.spec.ts │ └── plugins │ ├── .mocharc.json │ ├── package-lock.json │ ├── package.json │ ├── protocol.ts │ ├── provider.ts │ ├── test │ ├── matt.consumer.spec.ts │ └── matt.provider.spec.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── scripts ├── ci │ ├── build-and-test.ps1 │ ├── build-and-test.sh │ ├── lib │ │ ├── create_npmrc_file.sh │ │ ├── get-version.sh │ │ ├── prepare-release.sh │ │ ├── publish-9x.sh │ │ ├── publish.sh │ │ └── robust-bash.sh │ ├── release-9x.sh │ ├── release.sh │ └── test-examples.sh ├── deploy.ps1 ├── install-plugins ├── run-audit-fix-on-examples.sh ├── trigger-9x-release.sh └── trigger-release.sh ├── src ├── common │ ├── jsonTypes.ts │ ├── logger.ts │ ├── net.spec.ts │ ├── net.ts │ ├── request.spec.ts │ ├── request.ts │ └── spec.ts ├── dsl │ ├── apolloGraphql.spec.ts │ ├── apolloGraphql.ts │ ├── graphql.spec.ts │ ├── graphql.ts │ ├── interaction.spec.ts │ ├── interaction.ts │ ├── matchers.spec.ts │ ├── matchers.ts │ ├── message.ts │ ├── mockService.ts │ ├── options.ts │ └── verifier │ │ ├── index.ts │ │ ├── proxy │ │ ├── hooks.spec.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── messages.ts │ │ ├── parseBody.spec.ts │ │ ├── parseBody.ts │ │ ├── proxy.spec.ts │ │ ├── proxy.ts │ │ ├── proxyRequest.spec.ts │ │ ├── proxyRequest.ts │ │ ├── stateHandler │ │ │ ├── index.ts │ │ │ ├── setupStates.spec.ts │ │ │ ├── setupStates.ts │ │ │ ├── stateHandler.spec.ts │ │ │ └── stateHandler.ts │ │ ├── tracer.ts │ │ └── types.ts │ │ ├── types.ts │ │ ├── verifier.spec.ts │ │ └── verifier.ts ├── errors │ ├── configurationError.ts │ ├── graphQLQueryError.ts │ ├── matcherError.ts │ └── verificationError.ts ├── httpPact │ ├── ffi.spec.ts │ ├── ffi.ts │ ├── index.spec.ts │ ├── index.ts │ └── tracing.ts ├── index.ts ├── messageConsumerPact.spec.ts ├── messageConsumerPact.ts ├── messageProviderPact.spec.ts ├── messageProviderPact.ts ├── pact.integration.spec.ts ├── v3 │ ├── display.ts │ ├── ffi.ts │ ├── index.ts │ ├── matchers.spec.ts │ ├── matchers.ts │ ├── pact.ts │ ├── types.ts │ └── xml │ │ ├── xmlBuilder.ts │ │ ├── xmlElement.spec.ts │ │ ├── xmlElement.ts │ │ ├── xmlNode.ts │ │ └── xmlText.ts └── v4 │ ├── http │ ├── index.ts │ └── types.ts │ ├── index.ts │ ├── message │ ├── index.ts │ └── types.ts │ └── types.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "compact": false 4 | } 5 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | exclude_fingerprints: 5 | - e64415b948d76d28a63ee395869035f2 6 | - 31888c520d29b85472c4e2b6d08f7d94 7 | duplication: 8 | enabled: true 9 | config: 10 | languages: 11 | - javascript 12 | ratings: 13 | paths: 14 | - "src/**/*" 15 | exclude_paths: 16 | - "config/**/*" 17 | - "dist/**.js" 18 | - "examples/**/*" 19 | - "test/**/*" 20 | - "docs/**/*" 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | # Matches multiple files with brace expansion notation 16 | # Set default charset 17 | [*.{js}] 18 | charset = utf-8 19 | 20 | # Indentation override for all JS under lib directory 21 | [**/*.js] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | # Matches the exact files either package.json or .travis.yml 26 | [{package.json,.travis.yml}] 27 | indent_style = space 28 | indent_size = 2 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "import", "mocha", "chai-friendly"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "airbnb-base", 9 | "airbnb-typescript/base", 10 | "prettier" 11 | ], 12 | "settings": { 13 | "import/resolver": { 14 | "typescript": { 15 | "project": "tsconfig.json" 16 | } 17 | } 18 | }, 19 | "parserOptions": { 20 | "ecmaVersion": 2018, 21 | "sourceType": "module", 22 | "project": "tsconfig.json" 23 | }, 24 | "rules": { 25 | "@typescript-eslint/explicit-module-boundary-types": "error", 26 | "@typescript-eslint/no-unused-vars": "error", 27 | "@typescript-eslint/no-explicit-any": "error", 28 | "@typescript-eslint/no-shadow": "error", 29 | "@typescript-eslint/no-empty-function": [ 30 | "error", 31 | { "allow": ["constructors"] } 32 | ], 33 | "class-methods-use-this": "off", 34 | "import/prefer-default-export": "off", 35 | "no-underscore-dangle": ["error", { "allow": ["__pactMessageMetadata"] }] 36 | }, 37 | "overrides": [ 38 | { 39 | "files": ["**/*.spec.ts"], 40 | "extends": ["plugin:mocha/recommended"], 41 | "env": { 42 | "mocha": true 43 | }, 44 | "rules": { 45 | "@typescript-eslint/ban-ts-comment": "off", 46 | "@typescript-eslint/no-explicit-any": "off", 47 | "@typescript-eslint/no-empty-function": "off", 48 | "@typescript-eslint/no-unused-expressions": "off", 49 | "@typescript-eslint/no-unused-vars": [ 50 | "error", 51 | { "varsIgnorePattern": "unused" } 52 | ], 53 | "class-methods-use-this": "off", 54 | "chai-friendly/no-unused-expressions": "error", 55 | "mocha/no-mocha-arrows": "off", 56 | "mocha/no-setup-in-describe": "off", 57 | "no-new": "off" 58 | } 59 | } 60 | ], 61 | "globals": { 62 | "NodeJS": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: pact-foundation 4 | custom: ['https://pactflow.io'] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug with pact-js 4 | title: '' 5 | labels: bug, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | _Thank you for reporting a bug! We appreciate it very much. Issues are a big input into the priorities for Pact-JS development_ 11 | 12 | _All italic text in this template is safe to remove before submitting_ 13 | 14 | _Thanks again!_ 15 | 16 | ### Software versions 17 | 18 | _Please provide at least OS and version of pact-js_ 19 | 20 | - **OS**: _e.g. Mac OSX 10.11.5_ 21 | - **Consumer Pact library**: _e.g. Pact JS v2.6.0_ 22 | - **Provider Pact library**: _e.g. pact-jvm-provider-maven_2.11 v 3.3.8_ 23 | - **Node Version**: `node --version` 24 | 25 | ### Issue Checklist 26 | 27 | Please confirm the following: 28 | 29 | - [ ] I have upgraded to the latest 30 | - [ ] I have the read the FAQs in the Readme 31 | - [ ] I have triple checked, that there are **no unhandled promises** in my code and have [read](https://github.com/pact-foundation/pact-js/blob/master/docs/troubleshooting.md#test-fails-when-it-should-pass) the section on intermittent test failures 32 | - [ ] I have set my log level to debug and attached a log file showing the complete request/response cycle 33 | - [ ] For bonus points and virtual high fives, I have created a reproduceable git repository (see below) to illustrate the problem 34 | 35 | ### Expected behaviour 36 | 37 | _fill in here_ 38 | 39 | ### Actual behaviour 40 | 41 | _fill in here_ 42 | 43 | ### Steps to reproduce 44 | 45 | _How can someone else reproduce this bug?_ 46 | 47 | _Provide a [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). You can create your own repository, or use [this template](https://github.com/mefellows/pact-js-repro-template) that's already hooked up to CI and everything._ 48 | 49 | ### Relevant log files 50 | 51 | _Please ensure you set logging to `DEBUG` and attach any relevant log files here (or link to a gist)._ 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a new feature (or a modification to an existing feature) 4 | title: '' 5 | labels: enhancement, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | _Thank you for making a feature request! We appreciate it very much. GitHub Issues are a big input into the priorities for Pact-JS development_ 11 | 12 | _All italic text in this template is safe to remove before submitting_ 13 | 14 | _Thanks again!_ 15 | 16 | ### Checklist 17 | 18 | _This checklist is optional, but studies show that people who have followed it checklist are really excellent people and we like them_ 19 | 20 | Before making a feature request, I have: 21 | 22 | - [ ] [Searched the issues to check that this feature hasn't been requested before](https://github.com/pact-foundation/pact-js/issues?q=is%3Aissue) 23 | - [ ] Checked the documentation to see if it is possible to do what I want already 24 | 25 | ### Feature description 26 | 27 | _Please describe what you would like Pact-js to do_ 28 | 29 | ### Use case 30 | 31 | _What is the use case that motivates this feature request?_ 32 | 33 | _Please describe *why* you would like Pact-js to have this feature._ 34 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | _Thank you for reporting an issue! We appreciate it very much. Issues are a big input into the priorities for Pact-JS development_ 2 | 3 | _NOTE: If your issue is a how-to question, it is probably better asked in the pact-foundation slack channel, https://slack.pact.io/ _ 4 | 5 | _This is the template for issues that aren't feature requests or bug reports. If your issue is a feature request or bug report, please use the relevant template from https://github.com/pact-foundation/pact-js/issues/new/choose - these will cover off the most common questions we ask and speed up a fix_ 6 | 7 | _If you're reading this thinking "Hmmm, it is bug report/feature request, but neither of those templates work for what I want to report", then please feel free to ignore the template in whatever way you think best suits your issue. We trust you, you're awesome_ 8 | 9 | _All italic text in this template is safe to remove before submitting_ 10 | 11 | _Thanks again!_ 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thank you for making a pull request! 2 | 3 | Pact-JS is built and maintained by developers like you, and we appreciate contributions very much. You are awesome! 4 | 5 | Here is a short checklist to give your PR the best start: 6 | 7 | _Everything above can be removed once checked_ 8 | 9 | - [ ] `npm run dist` works locally (this will run tests, lint and build) 10 | - [ ] Commit messages are ready to go in the changelog (see below for details) 11 | - [ ] PR template filled in (see below for details) 12 | 13 | # Commit messages 14 | 15 | Our changelog is automatically built from our commit history, using conventional changelog. This means we'd like to take care that: 16 | 17 | - commit messages with the prefix `fix:` or `fix(foo):` are suitable to be added to the changelog under "Fixes and improvements" 18 | - commit messages with the prefix `feat:` or `feat(foo):` are suitable to be added to the changelog under "New features" 19 | 20 | If you've made many commits that don't adhere to this style, we recommend squashing 21 | your commits to a new branch before making a PR. Alternatively, we can do a squash 22 | merge, but you'll lose attribution for your change. 23 | 24 | For more information please see CONTRIBUTING.md 25 | 26 | _Everything above can be removed_ 27 | 28 | ### PR Template 29 | 30 | _Please describe what this PR is for, or link the issue that this PR fixes_ 31 | 32 | _You may add as much or as little context as you like here, whatever you think is right_ 33 | 34 | _Thanks again!_ 35 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:best-practices", 5 | ":pinOnlyDevDependencies" 6 | ], 7 | "prHourlyLimit": 0, 8 | "prConcurrentLimit": 0, 9 | "automerge": true 10 | } 11 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | commitsOnly: true 2 | allowMergeCommits: true 3 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - ready 10 | - good first issue 11 | - bug 12 | - enhancement 13 | - help wanted 14 | - blocked 15 | - Verified 16 | - question 17 | - Documentation 18 | - Triage 19 | 20 | # Label to use when marking an issue as stale 21 | staleLabel: needs attention 22 | 23 | # Comment to post when marking an issue as stale. Set to `false` to disable 24 | markComment: > 25 | This issue has been automatically marked as stale because it has not had 26 | recent activity. It will be closed if no further activity occurs. Thank you 27 | for your contributions. 28 | # Comment to post when closing a stale issue. Set to `false` to disable 29 | closeComment: false 30 | -------------------------------------------------------------------------------- /.github/workflows/publish-9x.yml: -------------------------------------------------------------------------------- 1 | name: Publish and release (9.x.x) 2 | 3 | on: 4 | repository_dispatch: 5 | types: 6 | - release-9x-triggered 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 13 | with: 14 | ref: 9.x.x 15 | fetch-depth: 0 16 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 17 | with: 18 | node-version: 14 19 | registry-url: 'https://registry.npmjs.org' 20 | - id: publish 21 | run: scripts/ci/release-9x.sh 22 | env: 23 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTOMATION_TOKEN}} 24 | - name: Create Release 25 | id: create_release 26 | uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 29 | with: 30 | tag_name: v${{ steps.publish.outputs.version }} 31 | release_name: Release v${{ steps.publish.outputs.version }} 32 | body: ${{steps.publish.outputs.notes}} 33 | draft: false 34 | prerelease: false 35 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish and release (latest) 2 | 3 | on: 4 | workflow_dispatch: 5 | repository_dispatch: 6 | types: 7 | - release-triggered 8 | 9 | env: 10 | GIT_COMMIT: ${{ github.sha }} 11 | GIT_REF: ${{ github.ref }} 12 | LOG_LEVEL: info 13 | PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }} 14 | PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }} 15 | 16 | jobs: 17 | release: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | version: v${{ steps.publish.outputs.version }} 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 23 | with: 24 | ref: master 25 | fetch-depth: 0 26 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 27 | with: 28 | node-version: 18 29 | registry-url: 'https://registry.npmjs.org' 30 | - id: publish 31 | run: scripts/ci/release.sh 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTOMATION_TOKEN}} 34 | - name: Create Release 35 | id: create_release 36 | uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 39 | with: 40 | tag_name: v${{ steps.publish.outputs.version }} 41 | release_name: Release v${{ steps.publish.outputs.version }} 42 | body: ${{steps.publish.outputs.notes}} 43 | draft: false 44 | prerelease: false 45 | -------------------------------------------------------------------------------- /.github/workflows/smartbear-issue-label-added.yml: -------------------------------------------------------------------------------- 1 | name: SmartBear Supported Issue Label Added 2 | 3 | on: 4 | issues: 5 | types: 6 | - labeled 7 | 8 | jobs: 9 | call-workflow: 10 | uses: pact-foundation/.github/.github/workflows/smartbear-issue-label-added.yml@master 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: Triage Issue 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - labeled 8 | pull_request: 9 | types: 10 | - labeled 11 | 12 | jobs: 13 | call-workflow: 14 | uses: pact-foundation/.github/.github/workflows/triage.yml@master 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/trigger_pact_docs_update.yml: -------------------------------------------------------------------------------- 1 | name: Trigger update to docs.pact.io 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - '**.md' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Trigger docs.pact.io update workflow 15 | uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3 16 | with: 17 | token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }} 18 | repository: pact-foundation/docs.pact.io 19 | event-type: pact-js-docs-updated 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs/ 3 | *.log 4 | npm-debug.log* 5 | .nyc_output 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | dist 27 | 28 | # Dependency directory 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # JSCPD 38 | jscpd.json 39 | 40 | # Pacts 41 | pacts/ 42 | 43 | # IDEs 44 | .idea 45 | *.iml 46 | .vscode 47 | *.swp 48 | 49 | # Local config files 50 | .nvmrc 51 | .envrc 52 | 53 | # Rust/neon output 54 | pact-foundation-pact-*.tgz 55 | /native/index.node 56 | /native/index.node.sha256 57 | /native/target/ 58 | build/ 59 | native/artifacts.json 60 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: src 3 | check: 4 | global: 5 | statements: 80 6 | lines: 80 7 | branches: 80 8 | functions: 75 9 | excludes: [] 10 | each: 11 | statements: 60 12 | lines: 60 13 | branches: 65 14 | functions: 75 15 | excludes: ['src/dsl/verifier.ts'] 16 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "require": ["tsx", "source-map-support/register"], 5 | "spec": ["src/**/*.spec.ts"], 6 | "exit": true 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # folders 2 | 3 | config/ 4 | coverage/ 5 | docs/ 6 | / 7 | logs/ 8 | pacts/ 9 | test/ 10 | examples/ 11 | 12 | # files 13 | .babelrc 14 | .codeclimate.yml 15 | .editorconfig 16 | .istanbul.yml 17 | .travis.yml 18 | jscpd.json 19 | ROADMAP.md 20 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["**/*.d.ts", "**/*.spec.ts"], 3 | "extension": [".ts"], 4 | "include": ["src/*.ts", "src/**/*.ts"], 5 | "instrument": true, 6 | "lines": 50, 7 | "reporter": ["text-summary", "html", "lcov"], 8 | "sourceMap": true, 9 | "statements": 50 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | README.md 3 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | '@pact-foundation/pact-js-prettier-config' 2 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=current 2 | FROM node:${NODE_VERSION}-alpine 3 | 4 | RUN apk add bash curl git file libc6-compat gcompat jq 5 | # libc6-compat gcompat required for @pact-foundation/pact-cli ruby runtime 6 | # all other dependencies just used for @pact-foundation/pact testing 7 | # so you don't need to use those in your own projects 8 | 9 | ENTRYPOINT [ "bash", "-c", "scripts/ci/build-and-test.sh" ] -------------------------------------------------------------------------------- /Dockerfile.debian: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=latest 2 | FROM node:${NODE_VERSION}-slim 3 | 4 | RUN apt-get -y update && apt-get -y install curl git jq 5 | # dependencies just used for @pact-foundation/pact testing 6 | # so you don't need to use those in your own projects 7 | 8 | ENTRYPOINT [ "bash", "-c", "scripts/ci/build-and-test.sh" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original work Copyright (c) 2014 DiUS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | We've moved to GitHub Actions for releases. 4 | 5 | ## How a release works 6 | 7 | Releases trigger when the repository receives the custom repository_dispatch event 8 | `release-triggered`. 9 | 10 | This triggers the `publish.yml` workflow, which in turn 11 | triggers the `release.sh` script in `scripts/ci`. 12 | The workflow will also create a github release with an appropriate changelog. 13 | 14 | Having the release triggered by a custom event is useful for automating 15 | releases in the future (eg for version bumps in pact dependencies). 16 | 17 | ### Release.sh 18 | 19 | This script is not intended to be run locally. Note that it modifies your git 20 | settings. 21 | 22 | The script will: 23 | 24 | - Modify git authorship settings 25 | - Confirm that there would be changes in the changelog after release 26 | - Run Lint 27 | - Run Build 28 | - Run Test 29 | - Commit an appropriate version bump, changelog and tag 30 | - Package and publish to npm 31 | - Push the new commit and tag back to the main branch. 32 | 33 | Should you need to modify the script locally, you will find it uses some 34 | dependencies in `scripts/ci/lib`. 35 | 36 | ## Kicking off a release 37 | 38 | You must be able to create a github access token with `repo` scope to the 39 | pact-js repository. 40 | 41 | - Set an environment variable `GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES` to this token. 42 | - Make sure master contains the code you want to release 43 | - Run `scripts/trigger-release.sh` 44 | 45 | Then wait for github to do its magic. It will release the current head of master. 46 | 47 | Note that the release script refuses to publish anything that wouldn't 48 | produce a changelog. Please make sure your commits follow the guidelines in 49 | `CONTRIBUTING.md` 50 | 51 | ## If the release fails 52 | 53 | The publish is the second to last step, so if the release fails, you don't 54 | need to do any rollbacks. 55 | 56 | However, there is a potential for the push to fail _after_ a publish if there 57 | are new commits to master since the release started. This is unlikely with 58 | the current commit frequency, but could still happen. Check the logs to 59 | determine if npm has a version that doesn't exist in the master branch. 60 | 61 | If this has happened, you will need to manually put the release commit in. 62 | 63 | ``` 64 | # First delete the new tag 65 | # somehow this ends up in the repository 66 | # even though the push fails. 67 | 68 | git checkout master 69 | git pull --tags 70 | git tag -d 71 | git push -delete origin 72 | 73 | 74 | # If there are changes that introduce features, then you'll have to branch and probably rebase 75 | 76 | # Now create a new commit + tag for the version: 77 | npm run release 78 | 79 | # Push that tag + commit 80 | git push origin master --follow-tags 81 | 82 | ``` 83 | 84 | - Don't forget to create a new release in github. 85 | 86 | Depending on the nature of the new commits to master after the release, you 87 | may need to rebase them on top of the tagged release commit and force push (only do this 88 | if the released version would be different to the version tagged by `npm run release`) 89 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # build version format 2 | version: "{build}" 3 | 4 | # fix lineendings in Windows 5 | init: 6 | - git config --global core.autocrlf input 7 | 8 | # Test against these versions of Node 9 | environment: 10 | matrix: 11 | - nodejs_version: "10" 12 | - nodejs_version: "12" 13 | - nodejs_version: "14" 14 | 15 | platform: 16 | - x64 17 | 18 | matrix: 19 | fast_finish: true 20 | skip_tags: true 21 | skip_branch_with_pr: true 22 | 23 | # Setup Node environment 24 | install: 25 | - ps: Install-Product node $env:nodejs_version $env:platform 26 | - node --version 27 | - npm --version 28 | - gcc --version 29 | - npm install 30 | 31 | # Run custom build script instead of MSBuild 32 | build_script: 33 | - powershell .\scripts\ci\build-and-test.ps1 34 | -------------------------------------------------------------------------------- /docs/diagrams/message-consumer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-js/9aeb7a443796cb0cb4e42b1472a0496e3749e12d/docs/diagrams/message-consumer.png -------------------------------------------------------------------------------- /docs/diagrams/message-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-js/9aeb7a443796cb0cb4e42b1472a0496e3749e12d/docs/diagrams/message-provider.png -------------------------------------------------------------------------------- /docs/diagrams/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-js/9aeb7a443796cb0cb4e42b1472a0496e3749e12d/docs/diagrams/summary.png -------------------------------------------------------------------------------- /docs/diagrams/workshop_step1_failed_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-js/9aeb7a443796cb0cb4e42b1472a0496e3749e12d/docs/diagrams/workshop_step1_failed_page.png -------------------------------------------------------------------------------- /docs/diagrams/workshop_step2_failed_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-js/9aeb7a443796cb0cb4e42b1472a0496e3749e12d/docs/diagrams/workshop_step2_failed_page.png -------------------------------------------------------------------------------- /docs/graphql.md: -------------------------------------------------------------------------------- 1 | ## GraphQL API 2 | 3 | GraphQL is simply an abstraction over HTTP and may be tested via Pact. 4 | 5 | ## Support 6 | 7 | | Role | Interface | Supported? | 8 | |:---------:|:--------------------:|:----------:| 9 | | Consumer | `Pact` | ✅ | 10 | | Consumer | `MessageConsumerPact` | ❌ | 11 | | Consumer | `PactV3` | ❌ | 12 | | Provider | `Verifier` | ✅ | 13 | | Provider | `MessageProviderPact` | ❌ | 14 | 15 | ### API 16 | 17 | There are two wrapper APIs available for GraphQL specific testing: `GraphQLInteraction` and `ApolloGraphQLInteraction` that can be used as a drop-in replacement for the `addInteraction` method. 18 | 19 | These are both lightweight wrappers over the standard DSL in order to make GraphQL testing a bit nicer. -------------------------------------------------------------------------------- /docs/xml.md: -------------------------------------------------------------------------------- 1 | # XML 2 | 3 | You can write both consumer and provider verification tests with XML requests or responses. 4 | 5 | ## Support 6 | 7 | | Role | Interface | Supported? | 8 | |:---------:|:--------------------:|:----------:| 9 | | Consumer | `Pact` | ❌ | 10 | | Consumer | `MessageConsumerPact` | ✅ | 11 | | Consumer | `PactV3` | ✅ | 12 | | Provider | `Verifier` | ✅ | 13 | | Provider | `MessageProviderPact` | ✅ | 14 | 15 | ## API 16 | 17 | The `XmlBuilder` class provides a DSL to help construct XML bodies with matching rules and generators. The generated JSON from the builder can be used as bodies in both Message and HTTP tests. 18 | 19 | ## Example 20 | 21 | 22 | ```js 23 | body: new XmlBuilder("1.0", "UTF-8", "ns1:projects").build((el) => { 24 | el.setAttributes({ 25 | id: "1234", 26 | "xmlns:ns1": "http://some.namespace/and/more/stuff", 27 | }) 28 | el.eachLike( 29 | "ns1:project", 30 | { 31 | id: integer(1), 32 | type: "activity", 33 | name: string("Project 1"), 34 | due: timestamp("yyyy-MM-dd'T'HH:mm:ss.SZ", "2016-02-11T09:46:56.023Z"), 35 | }, 36 | (project) => { 37 | project.appendElement("ns1:tasks", {}, (task) => { 38 | task.eachLike( 39 | "ns1:task", 40 | { 41 | id: integer(1), 42 | name: string("Task 1"), 43 | done: boolean(true), 44 | }, 45 | null, 46 | { examples: 5 } 47 | ) 48 | }) 49 | }, 50 | { examples: 2 } 51 | ) 52 | }) 53 | ``` 54 | 55 | #### Provider state callbacks 56 | 57 | Provider state callbacks have been updated to support parameters and return values. 58 | 59 | Simple callbacks run before the verification and receive optional parameters containing any key-value parameters defined in the pact file. 60 | 61 | The second form of callback accepts a `setup` and `teardown` function that execute on the lifecycle of the state setup. `setup` runs prior to the test, and `teardown` runs after the actual request has been sent to the provider. 62 | 63 | Provider state callbacks can also return a map of key-value values. These are used with provider-state injected values (see the section on that above). 64 | 65 | ```javascript 66 | stateHandlers: { 67 | // Simple state handler, runs before the verification 68 | "Has no animals": () => { 69 | return animalRepository.clear() 70 | }, 71 | // Runs only on setup phase (no teardown) 72 | "Has some animals": { 73 | setup: () => { 74 | return importData() 75 | } 76 | }, 77 | // Runs only on teardown phase (no setup) 78 | "Has a broken dependency": { 79 | setup: () => { 80 | // make some dependency fail... 81 | return Promise.resolve() 82 | }, 83 | teardown: () => { 84 | // fix the broken dependency! 85 | return Promise.resolve() 86 | } 87 | }, 88 | // Return provider specific IDs 89 | "Has an animal with ID": async (parameters) => { 90 | await importData() 91 | animalRepository.first().id = parameters.id 92 | return { 93 | description: `Animal with ID ${parameters.id} added to the db`, 94 | id: parameters.id, 95 | } 96 | }, 97 | ``` 98 | 99 | For a more detailed example, see the TODO project in the examples folder. -------------------------------------------------------------------------------- /examples/e2e/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | -------------------------------------------------------------------------------- /examples/e2e/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "exit": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/e2e/consumer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const request = require('superagent'); 3 | const server = express(); 4 | 5 | const getApiEndpoint = () => process.env.API_HOST || 'http://localhost:8081'; 6 | const authHeader = { 7 | Authorization: 'Bearer token', 8 | }; 9 | 10 | // Fetch animals who are currently 'available' from the 11 | // Animal Service 12 | const availableAnimals = () => { 13 | return request 14 | .get(`${getApiEndpoint()}/animals/available`) 15 | .set(authHeader) 16 | .then((res) => res.body); 17 | }; 18 | 19 | // Find animals by their ID from the Animal Service 20 | const getAnimalById = (id) => { 21 | return request 22 | .get(`${getApiEndpoint()}/animals/${id}`) 23 | .set(authHeader) 24 | .then( 25 | (res) => res.body, 26 | () => null 27 | ); 28 | }; 29 | 30 | // Suggestions function: 31 | // Given availability and sex etc. find available suitors, 32 | // and give them a 'score' 33 | const suggestion = (mate) => { 34 | const predicates = [ 35 | (candidate, animal) => candidate.id !== animal.id, 36 | (candidate, animal) => candidate.gender !== animal.gender, 37 | (candidate, animal) => candidate.animal === animal.animal, 38 | ]; 39 | 40 | const weights = [(candidate, animal) => Math.abs(candidate.age - animal.age)]; 41 | 42 | return availableAnimals().then((available) => { 43 | const eligible = available.filter( 44 | (a) => !predicates.map((p) => p(a, mate)).includes(false) 45 | ); 46 | 47 | return { 48 | suggestions: eligible.map((candidate) => { 49 | const score = weights.reduce((acc, weight) => { 50 | return acc - weight(candidate, mate); 51 | }, 100); 52 | 53 | return { 54 | score, 55 | animal: candidate, 56 | }; 57 | }), 58 | }; 59 | }); 60 | }; 61 | 62 | // Creates a mate for suggestions 63 | const createMateForDates = (mate) => { 64 | return request 65 | .post(`${getApiEndpoint()}/animals`) 66 | .send(mate) 67 | .set(authHeader) 68 | .set('Content-Type', 'application/json; charset=utf-8'); 69 | }; 70 | 71 | // Suggestions API 72 | server.get('/suggestions/:animalId', (req, res) => { 73 | if (!req.params.animalId) { 74 | res.writeHead(400); 75 | res.end(); 76 | } 77 | request 78 | .get(`${getApiEndpoint()}/animals/${req.params.animalId}`) 79 | .set(authHeader) 80 | .then((r) => { 81 | if (r.statusCode === 200) { 82 | suggestion(r.body).then((suggestions) => { 83 | res.json(suggestions); 84 | }); 85 | } else if (r && r.statusCode === 404) { 86 | res.writeHead(404); 87 | res.end(); 88 | } else { 89 | res.writeHead(500); 90 | res.end(); 91 | } 92 | }) 93 | .catch((err) => { 94 | console.log(err.message); 95 | res.writeHead(500); 96 | res.end(); 97 | }); 98 | }); 99 | 100 | module.exports = { 101 | server, 102 | availableAnimals, 103 | createMateForDates, 104 | suggestion, 105 | getAnimalById, 106 | }; 107 | -------------------------------------------------------------------------------- /examples/e2e/consumerService.js: -------------------------------------------------------------------------------- 1 | const { server } = require('./consumer.js'); 2 | 3 | server.listen(8080, () => { 4 | console.log('Animal Matching Service listening on http://localhost:8080'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/e2e/data/animalData.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "first_name": "Billy", 3 | "last_name": "Goat", 4 | "animal": "goat", 5 | "age": 21, 6 | "available_from": "2017-12-04T14:47:18.582Z", 7 | "gender": "M", 8 | "location": { 9 | "description": "Melbourne Zoo", 10 | "country": "Australia", 11 | "post_code": 3000 12 | }, 13 | "eligibility": { 14 | "available": true, 15 | "previously_married": false 16 | }, 17 | "interests": [ 18 | "walks in the garden/meadow", 19 | "munching on a paddock bomb", 20 | "parkour" 21 | ] 22 | }, 23 | { 24 | "first_name": "Nanny", 25 | "animal": "goat", 26 | "last_name": "Doe", 27 | "age": 27, 28 | "available_from": "2017-12-04T14:47:18.582Z", 29 | "gender": "F", 30 | "location": { 31 | "description": "Werribee Zoo", 32 | "country": "Australia", 33 | "post_code": 3000 34 | }, 35 | "eligibility": { 36 | "available": true, 37 | "previously_married": true 38 | }, 39 | "interests": [ 40 | "walks in the garden/meadow", 41 | "parkour" 42 | ] 43 | }, 44 | { 45 | "first_name": "Simba", 46 | "last_name": "Cantwaittobeking", 47 | "animal": "lion", 48 | "age": 4, 49 | "available_from": "2017-12-04T14:47:18.582Z", 50 | "gender": "M", 51 | "location": { 52 | "description": "Werribee Zoo", 53 | "country": "Australia", 54 | "post_code": 3000 55 | }, 56 | "eligibility": { 57 | "available": true, 58 | "previously_married": true 59 | }, 60 | "interests": [ 61 | "walks in the garden/meadow", 62 | "parkour" 63 | ] 64 | } 65 | ] 66 | -------------------------------------------------------------------------------- /examples/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "description": "Pact JS E2E Example", 5 | "scripts": { 6 | "test": "npm run test:consumer && npm run test:publish && npm run test:provider", 7 | "test:no:publish": "npm run test:consumer && npm run test:provider", 8 | "test:consumer": "mocha test/consumer.spec.js", 9 | "test:publish": "pact-broker publish ./pacts --consumer-app-version=\"$(npx absolute-version)\" --auto-detect-version-properties --broker-base-url=https://testdemo.pactflow.io", 10 | "test:provider": "mocha test/provider.spec.js", 11 | "can-i-deploy": "npm run can-i-deploy:consumer && npm run can-i-deploy:provider", 12 | "can-i-deploy:consumer": "pact-broker can-i-deploy --pacticipant 'Matching Service' --latest --broker-base-url https://testdemo.pactflow.io", 13 | "can-i-deploy:provider": "pact-broker can-i-deploy --pacticipant 'Animal Profile Service' --latest --broker-base-url https://testdemo.pactflow.io", 14 | "api": "concurrently 'npm run provider' 'npm run consumer'", 15 | "consumer": "node ./consumerService.js", 16 | "provider": "node ./providerService.js" 17 | }, 18 | "author": "matt.fellows@onegeek.com.au", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@pact-foundation/pact": "file:../../dist", 22 | "@pact-foundation/pact-cli": "^16.0.4", 23 | "absolute-version": "1.0.1", 24 | "chai": "^4.3.6", 25 | "chai-as-promised": "^7.1.1", 26 | "concurrently": "^7.3.0", 27 | "mocha": "^10.8.2" 28 | }, 29 | "dependencies": { 30 | "body-parser": "^1.20.0", 31 | "cors": "^2.8.5", 32 | "express": "^4.18.1", 33 | "superagent": "^8.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/e2e/provider.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const bodyParser = require('body-parser'); 4 | const Repository = require('./repository'); 5 | 6 | const server = express(); 7 | server.use(cors()); 8 | server.use(bodyParser.json()); 9 | server.use( 10 | bodyParser.urlencoded({ 11 | extended: true, 12 | }) 13 | ); 14 | server.use((req, res, next) => { 15 | res.header('Content-Type', 'application/json; charset=utf-8'); 16 | next(); 17 | }); 18 | 19 | server.use((req, res, next) => { 20 | const token = req.headers['authorization'] || ''; 21 | 22 | // TODO: revert back to 1234 23 | if (token !== 'Bearer token') { 24 | res.sendStatus(401).send(); 25 | } else { 26 | next(); 27 | } 28 | }); 29 | 30 | const animalRepository = new Repository(); 31 | 32 | // Load default data into a repository 33 | const importData = () => { 34 | const data = require('./data/animalData.json'); 35 | data.reduce((a, v) => { 36 | v.id = a + 1; 37 | animalRepository.insert(v); 38 | return a + 1; 39 | }, 0); 40 | }; 41 | 42 | // List all animals with 'available' eligibility 43 | const availableAnimals = () => { 44 | return animalRepository.fetchAll().filter((a) => { 45 | return a.eligibility.available; 46 | }); 47 | }; 48 | 49 | // Get all animals 50 | server.get('/animals', (req, res) => { 51 | res.json(animalRepository.fetchAll()); 52 | }); 53 | 54 | // Get all available animals 55 | server.get('/animals/available', (req, res) => { 56 | res.json(availableAnimals()); 57 | }); 58 | 59 | // Find an animal by ID 60 | server.get('/animals/:id', (req, res) => { 61 | const response = animalRepository.getById(req.params.id); 62 | if (response) { 63 | res.end(JSON.stringify(response)); 64 | } else { 65 | res.writeHead(404); 66 | res.end(); 67 | } 68 | }); 69 | 70 | // Register a new Animal for the service 71 | server.post('/animals', (req, res) => { 72 | const animal = req.body; 73 | 74 | // Really basic validation 75 | if (!animal || !animal.first_name) { 76 | res.writeHead(400); 77 | res.end(); 78 | 79 | return; 80 | } 81 | 82 | animal.id = animalRepository.fetchAll().length; 83 | animalRepository.insert(animal); 84 | 85 | res.json(animal); 86 | }); 87 | 88 | module.exports = { 89 | server, 90 | importData, 91 | animalRepository, 92 | }; 93 | -------------------------------------------------------------------------------- /examples/e2e/providerService.js: -------------------------------------------------------------------------------- 1 | const { server, importData } = require('./provider.js'); 2 | importData(); 3 | 4 | server.listen(8081, () => { 5 | console.log('Animal Profile Service listening on http://localhost:8081'); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/e2e/repository.js: -------------------------------------------------------------------------------- 1 | // Simple object repository 2 | class Repository { 3 | constructor() { 4 | this.entities = []; 5 | } 6 | 7 | fetchAll() { 8 | return this.entities; 9 | } 10 | 11 | getById(id) { 12 | return this.entities.find((entity) => id == entity.id); 13 | } 14 | 15 | insert(entity) { 16 | this.entities.push(entity); 17 | } 18 | 19 | clear() { 20 | this.entities = []; 21 | } 22 | } 23 | 24 | module.exports = Repository; 25 | -------------------------------------------------------------------------------- /examples/graphql/.gitignore: -------------------------------------------------------------------------------- 1 | src/*.js 2 | src/*.d.ts -------------------------------------------------------------------------------- /examples/graphql/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "require": ["tsx"], 5 | "exit": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/graphql/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL + Pact 2 | 3 | Demonstrates how to do Pact testing against a GraphQL endpoint. This is a POC, demonstrating how PactJS could provide a simple helper function that wraps the GraphQL request into a basic Interaction suitable for Pact (after all, GraphQL is simply an interface over HTTP). 4 | 5 | Test it out here: 6 | 7 | ``` 8 | npm run test:consumer 9 | npm run test:provider 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-pact-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "clean": "rimraf pacts", 9 | "test": "npm run clean && npm run test:consumer && npm run test:publish && npm run test:provider", 10 | "test:no:publish": "npm run test:consumer && npm run test:provider", 11 | "test:consumer": "mocha src/consumer.spec.ts", 12 | "test:publish": "pact-broker publish ./pacts --consumer-app-version=\"$(npx absolute-version)\" --auto-detect-version-properties --broker-base-url=https://testdemo.pactflow.io", 13 | "test:provider": "mocha src/provider.spec.ts" 14 | }, 15 | "keywords": [ 16 | "graphql", 17 | "pact", 18 | "contract-testing" 19 | ], 20 | "author": "Matt Fellows ", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@pact-foundation/pact": "file:../../dist", 24 | "@pact-foundation/pact-cli": "^16.0.4", 25 | "@types/chai": "^4.3.8", 26 | "@types/chai-as-promised": "7.1.6", 27 | "@types/mocha": "^10.0.2", 28 | "@types/node": "^18.7.11", 29 | "absolute-version": "1.0.2", 30 | "chai": "^4.3.10", 31 | "chai-as-promised": "^7.1.1", 32 | "mocha": "^10.2.0", 33 | "node-fetch": "^2.7.0", 34 | "rimraf": "^5.0.5", 35 | "tslint": "^5.20.1", 36 | "tslint-config-prettier": "^1.18.0", 37 | "tsx": "^4.19.2", 38 | "typescript": "^4.9.5" 39 | }, 40 | "dependencies": { 41 | "apollo-boost": "0.4.9", 42 | "apollo-cache-inmemory": "^1.6.6", 43 | "apollo-link-http": "^1.5.17", 44 | "express": "^4.21.2", 45 | "graphql": "^15.8.0", 46 | "graphql-http": "^1.22.0", 47 | "graphql-tag": "^2.12.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/graphql/src/consumer.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-expression object-literal-sort-keys max-classes-per-file no-empty */ 2 | import * as chai from 'chai'; 3 | import * as path from 'path'; 4 | import chaiAsPromised from 'chai-as-promised'; 5 | import { query } from './consumer'; 6 | import { 7 | Pact, 8 | GraphQLInteraction, 9 | Matchers, 10 | LogLevel, 11 | } from '@pact-foundation/pact'; 12 | const { like } = Matchers; 13 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 14 | 15 | const expect = chai.expect; 16 | 17 | chai.use(chaiAsPromised); 18 | 19 | describe('GraphQL example', () => { 20 | const provider = new Pact({ 21 | port: 4000, 22 | log: path.resolve(process.cwd(), 'logs', 'mockserver-integration.log'), 23 | dir: path.resolve(process.cwd(), 'pacts'), 24 | consumer: 'GraphQLConsumer', 25 | provider: 'GraphQLProvider', 26 | logLevel: LOG_LEVEL as LogLevel, 27 | }); 28 | 29 | before(() => provider.setup()); 30 | after(() => provider.finalize()); 31 | 32 | describe('query hello on /graphql', () => { 33 | before(() => { 34 | const graphqlQuery = new GraphQLInteraction() 35 | .uponReceiving('a hello request') 36 | .withQuery( 37 | ` 38 | query HelloQuery { 39 | hello 40 | } 41 | ` 42 | ) 43 | .withOperation('HelloQuery') 44 | .withRequest({ 45 | path: '/graphql', 46 | method: 'POST', 47 | }) 48 | .withVariables({ 49 | foo: 'bar', 50 | }) 51 | .willRespondWith({ 52 | status: 200, 53 | headers: { 54 | 'Content-Type': 'application/json; charset=utf-8', 55 | }, 56 | body: { 57 | data: { 58 | hello: like('Hello world!'), 59 | }, 60 | }, 61 | }); 62 | return provider.addInteraction(graphqlQuery); 63 | }); 64 | 65 | it('returns the correct response', () => { 66 | return expect(query()).to.eventually.deep.equal({ 67 | hello: 'Hello world!', 68 | }); 69 | }); 70 | 71 | // verify with Pact, and reset expectations 72 | afterEach(() => provider.verify()); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /examples/graphql/src/consumer.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-boost'; 2 | import { InMemoryCache } from 'apollo-cache-inmemory'; 3 | import gql from 'graphql-tag'; 4 | import { createHttpLink } from 'apollo-link-http'; 5 | 6 | const client = new ApolloClient({ 7 | cache: new InMemoryCache(), 8 | link: createHttpLink({ 9 | fetch: require('node-fetch'), 10 | headers: { 11 | foo: 'bar', 12 | }, 13 | uri: 'http://127.0.0.1:4000/graphql', 14 | }), 15 | }); 16 | 17 | export function query(): any { 18 | return client 19 | .query({ 20 | query: gql` 21 | query HelloQuery { 22 | hello 23 | } 24 | `, 25 | variables: { 26 | foo: 'bar', 27 | }, 28 | }) 29 | .then((result: any) => result.data); 30 | } 31 | -------------------------------------------------------------------------------- /examples/graphql/src/provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { Verifier, LogLevel } from '@pact-foundation/pact'; 2 | import { versionFromGitTag } from 'absolute-version'; 3 | import app from './provider'; 4 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 5 | 6 | let server: any; 7 | 8 | // Verify that the provider meets all consumer expectations 9 | describe('Pact Verification', () => { 10 | before((done) => { 11 | server = app.listen(4000, () => { 12 | done(); 13 | }); 14 | }); 15 | 16 | it('validates the expectations of Matching Service', () => { 17 | // lexical binding required here 18 | const opts = { 19 | // Local pacts 20 | // pactUrls: [path.resolve(process.cwd(), "./pacts/graphqlconsumer-graphqlprovider.json")], 21 | pactBrokerUrl: process.env.PACT_BROKER_BASE_URL, 22 | // If you're using the open source Pact Broker, use the username/password option as per below 23 | // pactBrokerUsername: process.env.PACT_BROKER_USERNAME 24 | // pactBrokerPassword: process.env.PACT_BROKER_PASSWORD 25 | // 26 | // if you're using a PactFlow broker, you must authenticate using the bearer token option 27 | // You can obtain the token from https://.pactflow.io/settings/api-tokens 28 | pactBrokerToken: process.env.PACT_BROKER_TOKEN, 29 | provider: 'GraphQLProvider', 30 | providerBaseUrl: 'http://localhost:4000/graphql', 31 | // Your version numbers need to be unique for every different version of your provider 32 | // see https://docs.pact.io/getting_started/versioning_in_the_pact_broker/ for details. 33 | // If you use git tags, then you can use absolute-version as we do here. 34 | providerVersion: versionFromGitTag(), 35 | publishVerificationResult: true, 36 | providerVersionBranch: process.env.GIT_BRANCH || 'master', 37 | 38 | // Find _all_ pacts that match the current provider branch 39 | consumerVersionSelectors: [ 40 | { 41 | matchingBranch: true, 42 | }, 43 | { 44 | mainBranch: true, 45 | }, 46 | { 47 | deployedOrReleased: true, 48 | }, 49 | ], 50 | logLevel: LOG_LEVEL as LogLevel, 51 | }; 52 | 53 | return new Verifier(opts).verifyProvider().then((output) => { 54 | server.close(); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /examples/graphql/src/provider.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { createHandler } from 'graphql-http/lib/use/express'; 3 | import { buildSchema } from 'graphql'; 4 | 5 | const schema = buildSchema(` 6 | type Query { 7 | hello: String 8 | } 9 | `); 10 | 11 | const root = { 12 | hello: () => 'Hello world!', 13 | }; 14 | 15 | const app = express(); 16 | export default app; 17 | 18 | app.use( 19 | '/graphql', 20 | createHandler({ 21 | rootValue: root, 22 | schema, 23 | }) 24 | ); 25 | 26 | export function start(): any { 27 | // tslint:disable:no-console 28 | app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); 29 | } 30 | -------------------------------------------------------------------------------- /examples/graphql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "noLib": false, 5 | "noImplicitReturns": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "emitDecoratorMetadata": true, 12 | "declaration": true, 13 | "experimentalDecorators": true, 14 | "target": "es5", 15 | "lib": ["es2016", "dom", "esnext.asynciterable"], 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/graphql/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "interface-name": [true, "never-prefix"], 7 | "no-var-requires": false, 8 | "ordered-imports": false, 9 | "semicolon": [true, "never"], 10 | "whitespace": [ 11 | true, 12 | "check-branch", 13 | "check-decl", 14 | "check-operator", 15 | "check-module", 16 | "check-separator", 17 | "check-rest-spread", 18 | "check-type", 19 | "check-typecast", 20 | "check-type-operator", 21 | "check-preblock" 22 | ] 23 | }, 24 | "rulesDirectory": [] 25 | } 26 | -------------------------------------------------------------------------------- /examples/jest/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "exit": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/jest/README.md: -------------------------------------------------------------------------------- 1 | # Jest Example 2 | 3 | 1. In the pact-js project root, change to the `examples/jest` directory 4 | 1. Run: `npm i` 5 | 1. Run the tests: `npm t` 6 | 7 | ## Comments about Jest 8 | 9 | To avoid race conditions if you have multiple pact specs, we recommend running Jest '[in band](https://facebook.github.io/jest/docs/en/cli.html#runinband)'. If you are running a large unit test suite you may want to run that separately as a result to take advantage of the concurrency of jest (although this is not always faster). To achieve this you can get your pact tests to have a suffix of '.pact.js' and add the following Jest argument to your pact task in npm: 10 | 11 | ``` 12 | --testRegex \"/*(.test.pact.js)\"" 13 | ``` 14 | 15 | This example uses [`jest-pact`](https://github.com/pact-foundation/jest-pact) 16 | -------------------------------------------------------------------------------- /examples/jest/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const axios = require('axios'); 4 | 5 | exports.getMeDogs = (endpoint) => { 6 | const url = endpoint.url; 7 | 8 | return axios 9 | .request({ 10 | method: 'GET', 11 | baseURL: url, 12 | url: '/dogs', 13 | headers: { Accept: 'application/json' }, 14 | }) 15 | .then((response) => response.data); 16 | }; 17 | 18 | exports.getMeCats = (endpoint) => { 19 | const url = endpoint.url; 20 | 21 | return axios 22 | .request({ 23 | method: 'GET', 24 | baseURL: url, 25 | url: '/cats?catId[]=2&catId[]=3', 26 | headers: { Accept: 'application/json' }, 27 | }) 28 | .then((response) => response.data); 29 | }; 30 | -------------------------------------------------------------------------------- /examples/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pact-example-jest", 3 | "version": "1.0.0", 4 | "description": "Jest Pact example", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rimraf pact", 8 | "pretest": "npm run clean", 9 | "test": "jest __tests__/ --runInBand", 10 | "test:publish": "pact-broker publish pact/pacts --consumer-app-version=\"$(npx absolute-version)\" --auto-detect-version-properties --broker-base-url=https://testdemo.pactflow.io" 11 | }, 12 | "license": "MIT", 13 | "jest": { 14 | "testEnvironment": "node" 15 | }, 16 | "devDependencies": { 17 | "@pact-foundation/pact": "file:../../dist", 18 | "@pact-foundation/pact-cli": "^16.0.4", 19 | "absolute-version": "1.0.2", 20 | "jest": "^29.7.0", 21 | "jest-pact": "^0.11.3", 22 | "rimraf": "^6.0.1" 23 | }, 24 | "dependencies": { 25 | "axios": "^1.8.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/messages/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /examples/messages/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "require": ["tsx"], 5 | "exit": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/messages/consumer/dog-handler.ts: -------------------------------------------------------------------------------- 1 | export type Dog = { 2 | id: string; 3 | type: string; 4 | name: string; 5 | }; 6 | 7 | // This is your message handler function. 8 | // It expects to receive a valid "dog" object 9 | // and returns a failed promise if not 10 | export function dogApiHandler(dog: Dog): void { 11 | if (!dog.id || !dog.name || !dog.type) { 12 | throw new Error('missing fields'); 13 | } 14 | 15 | // do some other things to dog... 16 | // e.g. dogRepository.save(dog) 17 | return; 18 | } 19 | -------------------------------------------------------------------------------- /examples/messages/consumer/message-consumer.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-expression object-literal-sort-keys max-classes-per-file no-empty */ 2 | 3 | import { 4 | Matchers, 5 | MessageConsumerPact, 6 | synchronousBodyHandler, 7 | LogLevel, 8 | } from '@pact-foundation/pact'; 9 | const { like, term } = Matchers; 10 | import { dogApiHandler } from './dog-handler'; 11 | 12 | const path = require('path'); 13 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 14 | 15 | describe('Message consumer tests', () => { 16 | const messagePact = new MessageConsumerPact({ 17 | consumer: 'MyJSMessageConsumer', 18 | dir: path.resolve(process.cwd(), 'pacts'), 19 | pactfileWriteMode: 'update', 20 | provider: 'MyJSMessageProvider', 21 | logLevel: LOG_LEVEL as LogLevel, 22 | }); 23 | 24 | describe('receive dog event', () => { 25 | it('accepts a valid dog', () => { 26 | return messagePact 27 | .given('a dog named drover') 28 | .expectsToReceive('a request for a dog') 29 | .withContent({ 30 | id: like(1), 31 | name: like('drover'), 32 | type: term({ 33 | generate: 'bulldog', 34 | matcher: '^(bulldog|sheepdog)$', 35 | }), 36 | }) 37 | .withMetadata({ 38 | queue: like('animals'), 39 | }) 40 | .verify(synchronousBodyHandler(dogApiHandler)); 41 | }); 42 | 43 | it('accepts a valid dog scenario 2', () => { 44 | return messagePact 45 | .given('a dog named rover') 46 | .expectsToReceive('a request for a dog') 47 | .withContent({ 48 | id: like(1), 49 | name: like('rover'), 50 | type: term({ 51 | generate: 'bulldog', 52 | matcher: '^(bulldog|sheepdog)$', 53 | }), 54 | }) 55 | .withMetadata({ 56 | queue: like('animals'), 57 | }) 58 | .verify(synchronousBodyHandler(dogApiHandler)); 59 | }); 60 | }); 61 | 62 | // This is an example of a pact breaking 63 | // uncomment to see how it works! 64 | it.skip('Does not accept an invalid dog', () => { 65 | return messagePact 66 | .given('some state') 67 | .expectsToReceive('a request for a dog') 68 | .withContent({ 69 | name: 'fido', 70 | }) 71 | .withMetadata({ 72 | 'content-type': 'application/json', 73 | }) 74 | .verify(synchronousBodyHandler(dogApiHandler)); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /examples/messages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pact-message-example", 3 | "version": "1.0.0", 4 | "description": "Example async message application", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "clean": "rimraf pacts", 9 | "test": "npm run test:consumer && npm run test:publish && npm run test:provider", 10 | "test:no:publish": "npm run test:consumer && npm run test:provider", 11 | "test:consumer": "mocha consumer/*.spec.ts", 12 | "test:provider": "mocha -t 20000 provider/*.spec.ts", 13 | "test:publish": "pact-broker publish ./pacts --consumer-app-version=\"$(npx absolute-version)\" --auto-detect-version-properties --broker-base-url=https://testdemo.pactflow.io" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@pact-foundation/pact": "file:../../dist", 19 | "@pact-foundation/pact-cli": "^16.0.4", 20 | "@types/mocha": "^9.1.1", 21 | "@types/node": "^18.7.11", 22 | "absolute-version": "1.0.1", 23 | "mocha": "^10.0.0", 24 | "rimraf": "^3.0.2", 25 | "tslint": "^5.20.1", 26 | "tslint-config-prettier": "^1.18.0", 27 | "tsx": "^4.19.2", 28 | "typescript": "^4.7.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/messages/provider/dog-client.ts: -------------------------------------------------------------------------------- 1 | // API integration client 2 | export function createDog(id: number): any { 3 | return new Promise((resolve, reject) => { 4 | resolve({ 5 | id, 6 | name: 'fido', 7 | type: 'bulldog', 8 | }); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /examples/messages/provider/message-provider.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable: no-console*/ 2 | import { 3 | LogLevel, 4 | MessageProviderPact, 5 | providerWithMetadata, 6 | } from '@pact-foundation/pact'; 7 | import { versionFromGitTag } from 'absolute-version'; 8 | const { createDog } = require('./dog-client'); 9 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 10 | 11 | describe('Message provider tests', () => { 12 | const p = new MessageProviderPact({ 13 | messageProviders: { 14 | 'a request for a dog': providerWithMetadata(() => createDog(27), { 15 | queue: 'animals', 16 | }), 17 | }, 18 | stateHandlers: { 19 | 'some state': () => { 20 | // TODO: prepare system useful in order to create a dog 21 | console.log('State handler: setting up "some state" for interaction'); 22 | return Promise.resolve(`state set to create a dog`); 23 | }, 24 | }, 25 | logLevel: LOG_LEVEL as LogLevel, 26 | provider: 'MyJSMessageProvider', 27 | // Your version numbers need to be unique for every different version of your provider 28 | // see https://docs.pact.io/getting_started/versioning_in_the_pact_broker/ for details. 29 | // If you use git tags, then you can use absolute-version as we do here. 30 | providerVersion: versionFromGitTag(), 31 | // For local validation 32 | // pactUrls: [path.resolve(process.cwd(), "pacts", "myjsmessageconsumer-myjsmessageprovider.json")], 33 | // Broker validation 34 | pactBrokerUrl: process.env.PACT_BROKER_BASE_URL, 35 | // If you're using the open source Pact Broker, use the username/password option as per below 36 | // pactBrokerUsername: process.env.PACT_BROKER_USERNAME 37 | // pactBrokerPassword: process.env.PACT_BROKER_PASSWORD 38 | // 39 | // if you're using a PactFlow broker, you must authenticate using the bearer token option 40 | // You can obtain the token from https://.pactflow.io/settings/api-tokens 41 | pactBrokerToken: process.env.PACT_BROKER_TOKEN, 42 | providerVersionBranch: process.env.GIT_BRANCH || 'master', 43 | 44 | // Find _all_ pacts that match the current provider branch 45 | consumerVersionSelectors: [ 46 | { 47 | matchingBranch: true, 48 | }, 49 | { 50 | mainBranch: true, 51 | }, 52 | { 53 | deployedOrReleased: true, 54 | }, 55 | ], 56 | }); 57 | 58 | describe('send a dog event', () => { 59 | it('sends a valid dog', () => { 60 | return p.verify(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/messages/provider/msg-client.ts: -------------------------------------------------------------------------------- 1 | export const getMsg = () => { 2 | return new Promise((resolve) => 3 | resolve({ 4 | body: { 5 | movPath: 'C:/xampp/htdocs/render-service/538481.mov', 6 | mp4Path: 'C:/xampp/htdocs/render-service/538481.mp4', 7 | projectId: 538481, 8 | projectPath: 'C:/xampp/htdocs/render-service/538481.aep', 9 | }, 10 | identifier: {}, 11 | }) 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /examples/messages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "noLib": false, 5 | "noImplicitReturns": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "moduleResolution": "node", 10 | "outDir": "dist", 11 | "noEmitOnError": true, 12 | "emitDecoratorMetadata": true, 13 | "declaration": true, 14 | "experimentalDecorators": true, 15 | "target": "es5", 16 | "lib": ["es2016", "dom", "esnext.asynciterable"] 17 | }, 18 | "include": ["consumer", "provider"], 19 | "exclude": ["./node_modules/**"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/messages/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "interface-name": [true, "never-prefix"], 7 | "no-var-requires": false, 8 | "ordered-imports": false, 9 | "semicolon": [true, "never"], 10 | "whitespace": [ 11 | true, 12 | "check-branch", 13 | "check-decl", 14 | "check-operator", 15 | "check-module", 16 | "check-separator", 17 | "check-rest-spread", 18 | "check-type", 19 | "check-typecast", 20 | "check-type-operator", 21 | "check-preblock" 22 | ] 23 | }, 24 | "rulesDirectory": [] 25 | } 26 | -------------------------------------------------------------------------------- /examples/mocha/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "exit": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/mocha/README.md: -------------------------------------------------------------------------------- 1 | # Mocha example 2 | 3 | The Mocha example is a really simple demonstration of the use of Pact in Mocha tests. 4 | -------------------------------------------------------------------------------- /examples/mocha/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const axios = require('axios'); 4 | 5 | exports.getMeDogs = (endpoint) => { 6 | const url = endpoint.url; 7 | const port = endpoint.port; 8 | 9 | return axios.request({ 10 | method: 'GET', 11 | baseURL: `${url}:${port}`, 12 | url: '/dogs', 13 | headers: { 14 | Accept: [ 15 | 'application/problem+json', 16 | 'application/json', 17 | 'text/plain', 18 | '*/*', 19 | ], 20 | }, 21 | }); 22 | }; 23 | 24 | exports.getMeDog = (endpoint) => { 25 | const url = endpoint.url; 26 | const port = endpoint.port; 27 | 28 | return axios.request({ 29 | method: 'GET', 30 | baseURL: `${url}:${port}`, 31 | url: '/dogs/1', 32 | headers: { Accept: 'application/json' }, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /examples/mocha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mochajs-pact-example", 3 | "version": "1.0.0", 4 | "description": "Mocha Pact example", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "rimraf pacts && mocha" 8 | }, 9 | "license": "MIT", 10 | "devDependencies": { 11 | "@pact-foundation/pact": "file:../../dist", 12 | "axios": "^1.8.4", 13 | "chai": "^4.3.6", 14 | "mocha": "^9.2.2", 15 | "rimraf": "^3.0.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/mocha/test/get-dogs.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const path = require('path'); 5 | const { Pact, Matchers } = require('@pact-foundation/pact'); 6 | const { getMeDogs, getMeDog } = require('../index'); 7 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 8 | 9 | describe('The Dog API', () => { 10 | let url = 'http://127.0.0.1'; 11 | const port = 8992; 12 | 13 | const provider = new Pact({ 14 | port: port, 15 | log: path.resolve(process.cwd(), 'logs', 'mockserver-integration.log'), 16 | dir: path.resolve(process.cwd(), 'pacts'), 17 | spec: 2, 18 | consumer: 'MyConsumer', 19 | provider: 'MyProvider', 20 | logLevel: LOG_LEVEL, 21 | }); 22 | 23 | const EXPECTED_BODY = [ 24 | { 25 | dog: 1, 26 | }, 27 | { 28 | dog: 2, 29 | }, 30 | ]; 31 | 32 | // Setup the provider 33 | before(() => provider.setup()); 34 | 35 | // Write Pact when all tests done 36 | after(() => provider.finalize()); 37 | 38 | // verify with Pact, and reset expectations 39 | afterEach(() => provider.verify()); 40 | 41 | describe('get /dogs', () => { 42 | before(() => { 43 | const interaction = { 44 | state: 'i have a list of dogs', 45 | uponReceiving: 'a request for all dogs', 46 | withRequest: { 47 | method: 'GET', 48 | path: '/dogs', 49 | headers: { 50 | // Accept: 'application/problem+json, application/json, text/plain, */*', // <- fails, must use array syntax ❌ 51 | Accept: [ 52 | 'application/problem+json', 53 | 'application/json', 54 | 'text/plain', 55 | '*/*', 56 | ], 57 | }, 58 | }, 59 | willRespondWith: { 60 | status: 200, 61 | headers: { 62 | 'Content-Type': 'application/json', 63 | }, 64 | body: EXPECTED_BODY, 65 | }, 66 | }; 67 | return provider.addInteraction(interaction); 68 | }); 69 | 70 | it('returns the correct response', async () => { 71 | const urlAndPort = { 72 | url: url, 73 | port: port, 74 | }; 75 | const response = await getMeDogs(urlAndPort); 76 | expect(response.data).to.eql(EXPECTED_BODY); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /examples/run-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit # Exit on error 4 | set -o nounset # Treat unset variables as an error 5 | 6 | EXAMPLES_DIR=$(cd -- "$(dirname "$0")" && pwd) 7 | 8 | usage() { 9 | echo "Usage: $0 [command]" 10 | echo 11 | echo "Run tests or a specific command in all examples" 12 | echo 13 | echo "Commands:" 14 | echo " Run all example tests (both 'npm ci' and 'npm test')" 15 | echo " Run a specific npm command (e.g., 'audit' for npm audit)" 16 | echo 17 | echo "Examples:" 18 | echo " $0" 19 | echo " $0 test" 20 | echo " $0 audit" 21 | exit 0 22 | } 23 | 24 | run_example() { 25 | local example_dir="$1" 26 | shift 27 | if [ $# -gt 0 ]; then 28 | echo "Ignoring extra arguments: $*" 29 | fi 30 | 31 | echo "$(tput bold)$(tput setaf 7)========================================$(tput sgr0)" 32 | echo "$(tput bold)$(tput setaf 7)Running example in $example_dir$(tput sgr0)" 33 | echo "$(tput bold)$(tput setaf 7)========================================$(tput sgr0)" 34 | 35 | if [ -d "$example_dir/node_modules" ]; then 36 | echo "$(tput bold)$(tput setaf 7)Skipping installation of dependencies$(tput sgr0)" 37 | else 38 | npm --prefix "$example_dir" ci 39 | fi 40 | 41 | echo "$(tput bold)$(tput setaf 7)Running tests$(tput sgr0)" 42 | npm --prefix "$example_dir" run test 43 | } 44 | 45 | run_cmd() { 46 | local example_dir="$1" 47 | shift 48 | if [ $# -eq 0 ]; then 49 | echo "No command provided" 50 | exit 1 51 | fi 52 | echo "$(tput bold)$(tput setaf 7)========================================$(tput sgr0)" 53 | echo "$(tput bold)$(tput setaf 7)Running '$*' in $example_dir$(tput sgr0)" 54 | echo "$(tput bold)$(tput setaf 7)========================================$(tput sgr0)" 55 | npm --prefix "$example_dir" "$@" 56 | } 57 | 58 | main() { 59 | if [ "${1-}" = "--help" ] || [ "${1-}" = "-h" ]; then 60 | usage 61 | fi 62 | 63 | declare -a examples=( 64 | "$EXAMPLES_DIR/e2e" 65 | "$EXAMPLES_DIR/graphql" 66 | "$EXAMPLES_DIR/jest" 67 | "$EXAMPLES_DIR/messages" 68 | "$EXAMPLES_DIR/mocha" 69 | "$EXAMPLES_DIR/serverless" 70 | "$EXAMPLES_DIR/typescript" 71 | "$EXAMPLES_DIR/v3/e2e" 72 | "$EXAMPLES_DIR/v3/provider-state-injected" 73 | "$EXAMPLES_DIR/v3/run-specific-verifications" 74 | "$EXAMPLES_DIR/v3/todo-consumer" 75 | "$EXAMPLES_DIR/v3/typescript" 76 | "$EXAMPLES_DIR/v4/matchers" 77 | "$EXAMPLES_DIR/v4/plugins" 78 | ) 79 | 80 | if [ $# -gt 0 ]; then 81 | for example in "${examples[@]}"; do 82 | run_cmd "$example" "$@" 83 | done 84 | else 85 | declare -a failed_examples=() 86 | for example in "${examples[@]}"; do 87 | run_example "$example" || failed_examples+=("$example") 88 | done 89 | 90 | if [ ${#failed_examples[@]} -ne 0 ]; then 91 | for failed_example in "${failed_examples[@]}"; do 92 | echo "$(tput bold)$(tput setaf 1)Failed to run example in $failed_example$(tput sgr0)" 93 | done 94 | fi 95 | fi 96 | } 97 | 98 | main "$@" 99 | -------------------------------------------------------------------------------- /examples/serverless/.gitignore: -------------------------------------------------------------------------------- 1 | .serverless 2 | pacts 3 | -------------------------------------------------------------------------------- /examples/serverless/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "exit": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/serverless/consumer/consumer.spec.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-expression object-literal-sort-keys max-classes-per-file no-empty */ 2 | const consumeEvent = require('./index').consumeEvent; 3 | const { 4 | MessageConsumerPact, 5 | Matchers, 6 | synchronousBodyHandler, 7 | } = require('@pact-foundation/pact'); 8 | const { like, term } = Matchers; 9 | const path = require('path'); 10 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 11 | 12 | describe('Serverless consumer tests', () => { 13 | const messagePact = new MessageConsumerPact({ 14 | consumer: 'SNSPactEventConsumer', 15 | dir: path.resolve(process.cwd(), 'pacts'), 16 | provider: 'SNSPactEventProvider', 17 | logLevel: LOG_LEVEL, 18 | }); 19 | 20 | describe('receive a pact event', () => { 21 | it('accepts a valid event', () => { 22 | return messagePact 23 | .expectsToReceive('a request to save an event') 24 | .withContent({ 25 | id: like(1), 26 | event: like('something important'), 27 | type: term({ generate: 'save', matcher: '^(save|update|cancel)$' }), 28 | }) 29 | .withMetadata({ 30 | 'content-type': 'application/json', 31 | }) 32 | .verify(synchronousBodyHandler(consumeEvent)); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/serverless/consumer/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Consumer handler, responsible for extracting message from SNS 4 | // and dealing with lambda-related things. 5 | const handler = (event, context, callback) => { 6 | console.log('Received event from SNS'); 7 | 8 | event.Records.forEach((e) => { 9 | console.log('Event:', JSON.parse(e.Sns.Message)); 10 | consumeEvent(JSON.parse(e.Sns.Message)); 11 | }); 12 | 13 | callback(null, { 14 | event, 15 | }); 16 | }; 17 | 18 | let eventCount = 0; 19 | 20 | // Actual consumer code, has no Lambda/AWS/Protocol specific stuff 21 | // This is the thing we test in the Consumer Pact tests 22 | const consumeEvent = (event) => { 23 | console.log('consuming event', event); 24 | 25 | if (!event || !event.id) { 26 | throw new Error('Invalid event, missing fields'); 27 | } 28 | 29 | // You'd normally do something useful, like process it 30 | // and save it in Dynamo 31 | console.log('Event count:', ++eventCount); 32 | 33 | return eventCount; 34 | }; 35 | 36 | module.exports = { 37 | handler, 38 | consumeEvent, 39 | }; 40 | -------------------------------------------------------------------------------- /examples/serverless/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-pact-example", 3 | "version": "1.0.0", 4 | "description": "Example testing a serverless app with Pact", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rimraf pacts", 8 | "test": "npm run test:consumer && npm run test:publish && npm run test:provider", 9 | "test:no:publish": "npm run test:consumer && npm run test:provider", 10 | "test:consumer": "mocha consumer/*.spec.js", 11 | "test:provider": "mocha -t 30000 provider/*.spec.js", 12 | "test:publish": "pact-broker publish ./pacts --consumer-app-version=\"$(npx absolute-version)\" --auto-detect-version-properties --broker-base-url=https://testdemo.pactflow.io", 13 | "can-i-deploy": "npm run can-i-deploy:consumer && npm run can-i-deploy:provider", 14 | "can-i-deploy:consumer": "$(find ../../ -name pact-broker | grep -e 'bin/pact-broker$' | head -n 1) can-i-deploy --pacticipant SNSPactEventConsumer --latest --broker-base-url https://testdemo.pactflow.io", 15 | "can-i-deploy:provider": "$(find ../../ -name pact-broker | grep -e 'bin/pact-broker$' | head -n 1) can-i-deploy --pacticipant SNSPactEventProvider --latest --broker-base-url https://testdemo.pactflow.io", 16 | "create-stack": "serverless deploy --verbose", 17 | "deploy": "npm run deploy:consumer && npm run deploy:provider", 18 | "deploy:consumer": "npm run can-i-deploy && serverless deploy -f consumer", 19 | "deploy:provider": "npm run can-i-deploy && serverless deploy -f provider" 20 | }, 21 | "devDependencies": { 22 | "@pact-foundation/pact": "file:../../dist", 23 | "@pact-foundation/pact-cli": "^16.0.4", 24 | "absolute-version": "1.0.1", 25 | "mocha": "^10.0.0", 26 | "rimraf": "^3.0.2", 27 | "serverless": "^3.22.0" 28 | }, 29 | "dependencies": { 30 | "aws-sdk": "^2.1354.0" 31 | }, 32 | "keywords": [ 33 | "pact", 34 | "serverless", 35 | "lambda", 36 | "contract-testing" 37 | ], 38 | "author": "Matt Fellows ", 39 | "license": "MIT" 40 | } 41 | -------------------------------------------------------------------------------- /examples/serverless/provider/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless -------------------------------------------------------------------------------- /examples/serverless/provider/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AWS = require('aws-sdk'); 4 | 5 | const TOPIC_ARN = process.env.TOPIC_ARN; 6 | 7 | // Handler is the Lambda and SNS specific code 8 | // The message generation logic is separated from the handler itself 9 | // in the 10 | const handler = (event, context, callback) => { 11 | const message = createEvent(); 12 | 13 | const sns = new AWS.SNS(); 14 | 15 | const params = { 16 | Message: JSON.stringify(message), 17 | TopicArn: TOPIC_ARN, 18 | }; 19 | 20 | sns.publish(params, (error, data) => { 21 | if (error) { 22 | callback(error); 23 | } 24 | 25 | callback(null, { 26 | message: 'Message successfully published to SNS topic "pact-events"', 27 | event, 28 | }); 29 | }); 30 | 31 | callback(null, message); 32 | }; 33 | 34 | // Separate your producer code, from the lambda handler. 35 | // No Lambda/AWS/Protocol specific stuff in here.. 36 | const createEvent = (obj) => { 37 | // Change 'type' to something else to test a pact failure 38 | return { 39 | id: parseInt(Math.random() * 100), 40 | event: 'an update to something useful', 41 | type: 'update', 42 | }; 43 | }; 44 | 45 | module.exports = { 46 | handler, 47 | createEvent, 48 | }; 49 | -------------------------------------------------------------------------------- /examples/serverless/provider/message-provider.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | MessageProviderPact, 3 | providerWithMetadata, 4 | } = require('@pact-foundation/pact'); 5 | const { versionFromGitTag } = require('absolute-version'); 6 | const path = require('path'); 7 | const { createEvent } = require('./index'); 8 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 9 | 10 | describe('Message provider tests', () => { 11 | const p = new MessageProviderPact({ 12 | messageProviders: { 13 | 'a request to save an event': providerWithMetadata(() => createEvent(), { 14 | 'content-type': 'application/json', 15 | }), 16 | }, 17 | logLevel: LOG_LEVEL, 18 | provider: 'SNSPactEventProvider', 19 | 20 | // Your version numbers need to be unique for every different version of your provider 21 | // see https://docs.pact.io/getting_started/versioning_in_the_pact_broker/ for details. 22 | // If you use git tags, then you can use absolute-version as we do here. 23 | providerVersion: versionFromGitTag(), 24 | 25 | // For local validation 26 | // pactUrls: [path.resolve(process.cwd(), "pacts", "snspacteventconsumer-snspacteventprovider.json")], 27 | 28 | // Uncomment to use the broker 29 | pactBrokerUrl: process.env.PACT_BROKER_BASE_URL, 30 | // If you're using the open source Pact Broker, use the username/password option as per below 31 | // pactBrokerUsername: process.env.PACT_BROKER_USERNAME 32 | // pactBrokerPassword: process.env.PACT_BROKER_PASSWORD 33 | // 34 | // if you're using a PactFlow broker, you must authenticate using the bearer token option 35 | // You can obtain the token from https://.pactflow.io/settings/api-tokens 36 | pactBrokerToken: process.env.PACT_BROKER_TOKEN, 37 | publishVerificationResult: true, 38 | 39 | providerVersionBranch: process.env.GIT_BRANCH || 'master', 40 | 41 | // Find _all_ pacts that match the current provider branch 42 | consumerVersionSelectors: [ 43 | { 44 | matchingBranch: true, 45 | }, 46 | { 47 | mainBranch: true, 48 | }, 49 | { 50 | deployedOrReleased: true, 51 | }, 52 | ], 53 | }); 54 | 55 | describe('send an event', () => { 56 | it('sends a valid event', () => { 57 | return p.verify(); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/serverless/serverless.yml: -------------------------------------------------------------------------------- 1 | # Serverless SNS 2 | service: pact-events 3 | 4 | provider: 5 | name: aws 6 | runtime: nodejs12.x 7 | iamRoleStatements: 8 | - Effect: "Allow" 9 | Resource: "*" 10 | Action: 11 | - "sns:*" 12 | environment: 13 | TOPIC_ARN: { "Fn::Join": ["", ["arn:aws:sns:us-east-1:", { "Ref": "AWS::AccountId" }, ":pact-events"] ] } 14 | 15 | package: 16 | individually: true 17 | exclude: 18 | - node_modules/** 19 | 20 | functions: 21 | provider: 22 | handler: provider/index.handler 23 | consumer: 24 | handler: consumer/index.handler 25 | events: 26 | - sns: pact-events 27 | -------------------------------------------------------------------------------- /examples/typescript/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "require": ["tsx"], 5 | "exit": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-pact", 3 | "version": "1.0.0", 4 | "description": "TypeScript Pact Example", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/*.spec.ts" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@pact-foundation/pact": "file:../../dist", 13 | "@types/chai": "^4.3.3", 14 | "@types/chai-as-promised": "7.1.5", 15 | "@types/mocha": "^9.1.1", 16 | "@types/node": "^18.7.11", 17 | "@types/sinon-chai": "^3.2.8", 18 | "chai": "^4.3.6", 19 | "chai-as-promised": "^7.1.1", 20 | "mocha": "^10.0.0", 21 | "sinon": "^14.0.0", 22 | "sinon-chai": "^3.7.0", 23 | "tslint": "^5.20.1", 24 | "tslint-config-prettier": "^1.18.0", 25 | "tsx": "^4.19.2", 26 | "typescript": "^4.7.4" 27 | }, 28 | "dependencies": { 29 | "axios": "^1.8.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosPromise } from 'axios'; 2 | export class DogService { 3 | private url: string; 4 | private port: number; 5 | 6 | constructor(endpoint: any) { 7 | this.url = endpoint.url; 8 | this.port = endpoint.port; 9 | } 10 | 11 | public getMeDogs = (): AxiosPromise => { 12 | return axios.request({ 13 | baseURL: `${this.url}:${this.port}`, 14 | headers: { Accept: 'application/json' }, 15 | method: 'GET', 16 | url: '/dogs', 17 | }); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /examples/typescript/test/get-dog.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-expression object-literal-sort-keys max-classes-per-file no-empty */ 2 | import * as chai from 'chai'; 3 | import chaiAsPromised from 'chai-as-promised'; 4 | import path = require('path'); 5 | import sinonChai from 'sinon-chai'; 6 | import { Pact, Interaction, Matchers, LogLevel } from '@pact-foundation/pact'; 7 | 8 | const expect = chai.expect; 9 | import { DogService } from '../src/index'; 10 | const { eachLike } = Matchers; 11 | 12 | chai.use(sinonChai); 13 | chai.use(chaiAsPromised); 14 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 15 | 16 | describe('The Dog API', () => { 17 | const url = 'http://127.0.0.1'; 18 | let dogService: DogService; 19 | 20 | const provider = new Pact({ 21 | // port, 22 | log: path.resolve(process.cwd(), 'logs', 'mockserver-integration.log'), 23 | dir: path.resolve(process.cwd(), 'pacts'), 24 | spec: 2, 25 | consumer: 'Typescript Consumer Example', 26 | provider: 'Typescript Provider Example', 27 | logLevel: LOG_LEVEL as LogLevel, 28 | }); 29 | 30 | const dogExample = { dog: 1 }; 31 | const EXPECTED_BODY = eachLike(dogExample); 32 | 33 | before(() => 34 | provider.setup().then((opts) => { 35 | dogService = new DogService({ url, port: opts.port }); 36 | }) 37 | ); 38 | 39 | after(() => provider.finalize()); 40 | 41 | afterEach(() => provider.verify()); 42 | 43 | describe('get /dogs using builder pattern', () => { 44 | before(() => { 45 | const interaction = new Interaction() 46 | .given('I have a list of dogs') 47 | .uponReceiving('a request for all dogs with the builder pattern') 48 | .withRequest({ 49 | method: 'GET', 50 | path: '/dogs', 51 | headers: { 52 | Accept: 'application/json', 53 | }, 54 | }) 55 | .willRespondWith({ 56 | status: 200, 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | }, 60 | body: EXPECTED_BODY, 61 | }); 62 | 63 | return provider.addInteraction(interaction); 64 | }); 65 | 66 | it('returns the correct response', async () => { 67 | const res = await dogService.getMeDogs(); 68 | expect(res.data[0]).to.deep.eq(dogExample); 69 | }); 70 | }); 71 | 72 | describe('get /dogs using object pattern', () => { 73 | before(() => { 74 | return provider.addInteraction({ 75 | state: 'i have a list of dogs', 76 | uponReceiving: 'a request for all dogs with the object pattern', 77 | withRequest: { 78 | method: 'GET', 79 | path: '/dogs', 80 | headers: { 81 | Accept: 'application/json', 82 | }, 83 | }, 84 | willRespondWith: { 85 | status: 200, 86 | headers: { 87 | 'Content-Type': 'application/json', 88 | }, 89 | body: EXPECTED_BODY, 90 | }, 91 | }); 92 | }); 93 | 94 | it('returns the correct response', async () => { 95 | const res = await dogService.getMeDogs(); 96 | expect(res.data[0]).to.deep.eq(dogExample); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "noLib": false, 5 | "noImplicitReturns": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "emitDecoratorMetadata": true, 12 | "declaration": true, 13 | "experimentalDecorators": true, 14 | "target": "es5", 15 | "lib": ["es2016", "dom", "esnext.asynciterable"] 16 | }, 17 | "include": ["src"], 18 | "exclude": ["./node_modules/**"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/typescript/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "interface-name": [true, "never-prefix"], 7 | "no-var-requires": false, 8 | "ordered-imports": false, 9 | "semicolon": [true, "never"], 10 | "whitespace": [ 11 | true, 12 | "check-branch", 13 | "check-decl", 14 | "check-operator", 15 | "check-module", 16 | "check-separator", 17 | "check-rest-spread", 18 | "check-type", 19 | "check-typecast", 20 | "check-type-operator", 21 | "check-preblock" 22 | ] 23 | }, 24 | "rulesDirectory": [] 25 | } 26 | -------------------------------------------------------------------------------- /examples/v3/e2e/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | -------------------------------------------------------------------------------- /examples/v3/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | -------------------------------------------------------------------------------- /examples/v3/e2e/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "exit": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/v3/e2e/consumerService.js: -------------------------------------------------------------------------------- 1 | const { server } = require('./consumer.js'); 2 | 3 | server.listen(8080, () => { 4 | console.log('Animal Matching Service listening on http://localhost:8080'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/v3/e2e/data/animalData.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "first_name": "Billy", 3 | "last_name": "Goat", 4 | "animal": "goat", 5 | "age": 21, 6 | "available_from": "2017-12-04T14:47:18.582Z", 7 | "gender": "M", 8 | "location": { 9 | "description": "Melbourne Zoo", 10 | "country": "Australia", 11 | "post_code": 3000 12 | }, 13 | "eligibility": { 14 | "available": true, 15 | "previously_married": false 16 | }, 17 | "interests": [ 18 | "walks in the garden/meadow", 19 | "munching on a paddock bomb", 20 | "parkour" 21 | ], 22 | "identifiers": { 23 | "001": { 24 | "id": "001", 25 | "description": "thing 001" 26 | }, 27 | "007": { 28 | "id": "007", 29 | "description": "thing 007" 30 | } 31 | } 32 | }, 33 | { 34 | "first_name": "Nanny", 35 | "animal": "goat", 36 | "last_name": "Doe", 37 | "age": 27, 38 | "available_from": "2017-12-04T14:47:18.582Z", 39 | "gender": "F", 40 | "location": { 41 | "description": "Werribee Zoo", 42 | "country": "Australia", 43 | "post_code": 3000 44 | }, 45 | "eligibility": { 46 | "available": true, 47 | "previously_married": true 48 | }, 49 | "interests": [ 50 | "walks in the garden/meadow", 51 | "parkour" 52 | ], 53 | "identifiers": { 54 | "003": { 55 | "id": "003", 56 | "description": "thing 3" 57 | } 58 | } 59 | }, 60 | { 61 | "first_name": "Simba", 62 | "last_name": "Cantwaittobeking", 63 | "animal": "lion", 64 | "age": 4, 65 | "available_from": "2017-12-04T14:47:18.582Z", 66 | "gender": "M", 67 | "location": { 68 | "description": "Werribee Zoo", 69 | "country": "Australia", 70 | "post_code": 3000 71 | }, 72 | "eligibility": { 73 | "available": true, 74 | "previously_married": true 75 | }, 76 | "interests": [ 77 | "walks in the garden/meadow", 78 | "parkour" 79 | ], 80 | "identifiers": { 81 | "004": { 82 | "id": "004", 83 | "description": "thing 004" 84 | } 85 | } 86 | } 87 | ] 88 | -------------------------------------------------------------------------------- /examples/v3/e2e/data/data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/v3/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "description": "Pact JS E2E Example", 5 | "scripts": { 6 | "test": "npm run test:consumer && npm run test:publish && npm run test:provider", 7 | "test:no:publish": "npm run test:consumer && npm run test:provider", 8 | "test:consumer": "mocha test/consumer.spec.js", 9 | "test:publish": "pact-broker publish ./pacts --consumer-app-version=\"$(npx absolute-version)\" --auto-detect-version-properties --broker-base-url=https://testdemo.pactflow.io", 10 | "test:provider": "mocha test/provider.spec.js", 11 | "can-i-deploy": "npm run can-i-deploy:consumer && npm run can-i-deploy:provider", 12 | "can-i-deploy:consumer": "pact-broker can-i-deploy --pacticipant 'Matching Service' --latest --broker-base-url https://testdemo.pactflow.io", 13 | "can-i-deploy:provider": "pact-broker can-i-deploy --pacticipant 'Animal Profile Service' --latest --broker-base-url https://testdemo.pactflow.io", 14 | "api": "concurrently 'npm run provider' 'npm run consumer'", 15 | "consumer": "node ./consumerService.js", 16 | "provider": "node ./providerService.js" 17 | }, 18 | "author": "matt.fellows@onegeek.com.au", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@pact-foundation/pact": "file:../../../dist", 22 | "@pact-foundation/pact-cli": "^16.0.4", 23 | "absolute-version": "1.0.1", 24 | "chai": "^3.5.0", 25 | "chai-as-promised": "^7.1.1", 26 | "concurrently": "^7.3.0", 27 | "mocha": "^10.0.0" 28 | }, 29 | "dependencies": { 30 | "body-parser": "^1.20.0", 31 | "cors": "^2.8.5", 32 | "express": "^4.18.1", 33 | "superagent": "^8.0.0", 34 | "xml": "^1.0.1" 35 | }, 36 | "config": { 37 | "pact_do_not_track": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/v3/e2e/providerService.js: -------------------------------------------------------------------------------- 1 | const { server, importData } = require('./provider.js'); 2 | importData(); 3 | 4 | server.listen(8081, () => { 5 | console.log('Animal Profile Service listening on http://localhost:8081'); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/v3/e2e/repository.js: -------------------------------------------------------------------------------- 1 | // Simple object repository 2 | class Repository { 3 | constructor() { 4 | this.entities = []; 5 | } 6 | 7 | fetchAll() { 8 | return this.entities; 9 | } 10 | 11 | getById(id) { 12 | return this.entities.find((entity) => id == entity.id); 13 | } 14 | 15 | insert(entity) { 16 | this.entities.push(entity); 17 | } 18 | 19 | clear() { 20 | this.entities = []; 21 | } 22 | 23 | first() { 24 | return this.entities[0]; 25 | } 26 | 27 | count() { 28 | return this.entities.length; 29 | } 30 | } 31 | 32 | module.exports = Repository; 33 | -------------------------------------------------------------------------------- /examples/v3/provider-state-injected/README.md: -------------------------------------------------------------------------------- 1 | # Provider state injected example 2 | 3 | This example follows what is described in the blog post https://pactflow.io/blog/injecting-values-from-provider-states/ 4 | 5 | -------------------------------------------------------------------------------- /examples/v3/provider-state-injected/consumer/transaction-service.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | let accountServiceUrl = ''; 4 | 5 | module.exports = { 6 | setAccountServiceUrl: (url) => { 7 | accountServiceUrl = url; 8 | }, 9 | 10 | createTransaction: (accountId, amountInCents) => { 11 | return axios 12 | .get(accountServiceUrl + '/accounts/search/findOneByAccountNumberId', { 13 | params: { 14 | accountNumber: accountId, 15 | }, 16 | headers: { 17 | Accept: 'application/hal+json', 18 | }, 19 | }) 20 | .then(({ data }) => { 21 | // This is the point where a real transaction service would create the transaction, but for the purpose 22 | // of this example we'll assume this has happened here 23 | let id = Math.floor(Math.random() * Math.floor(100000)); 24 | return { 25 | account: { 26 | accountNumber: data.accountNumber.id, 27 | accountReference: data.accountRef, 28 | }, 29 | transaction: { 30 | id, 31 | amount: amountInCents, 32 | }, 33 | }; 34 | }); 35 | }, 36 | 37 | // same as createTransaction, but demonstrating using a PostBody 38 | createTransactionWithPostBody: (accountId, amountInCents) => { 39 | return axios 40 | .post( 41 | accountServiceUrl + '/accounts/search/findOneByAccountNumberIdInBody', 42 | { 43 | accountNumber: accountId, 44 | }, 45 | { 46 | headers: { 47 | Accept: 'application/hal+json', 48 | }, 49 | } 50 | ) 51 | .then(({ data }) => { 52 | // This is the point where a real transaction service would create the transaction, but for the purpose 53 | // of this example we'll assume this has happened here 54 | let id = Math.floor(Math.random() * Math.floor(100000)); 55 | return { 56 | account: { 57 | accountNumber: data.accountNumber.id, 58 | accountReference: data.accountRef, 59 | }, 60 | transaction: { 61 | id, 62 | amount: amountInCents, 63 | }, 64 | }; 65 | }); 66 | }, 67 | 68 | getText: (id) => { 69 | return axios.get(accountServiceUrl + '/data/' + id).then((data) => { 70 | return data; 71 | }); 72 | }, 73 | getXml: (id) => { 74 | return axios.get(accountServiceUrl + '/data/xml/' + id).then((data) => { 75 | return data; 76 | }); 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /examples/v3/provider-state-injected/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "provider-state-injected", 3 | "version": "1.0.0", 4 | "description": "Example project showing provider state injected values", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run test:consumer && npm run test:provider", 8 | "test:consumer": "jest --runInBand --detectOpenHandles --forceExit consumer/transaction-service.test.js", 9 | "test:provider": "jest --runInBand --detectOpenHandles --forceExit -i --testTimeout 30000 provider/account-service.test.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@pact-foundation/pact": "file:../../../dist", 15 | "chai": "^4.3.6", 16 | "jest": "^29.6.0" 17 | }, 18 | "dependencies": { 19 | "axios": "^1.8.4", 20 | "body-parser": "^1.20.3", 21 | "cors": "^2.8.5", 22 | "express": "^4.17.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/v3/provider-state-injected/provider/account-repository.js: -------------------------------------------------------------------------------- 1 | class AccountNumber { 2 | constructor(num) { 3 | this.id = num; 4 | } 5 | } 6 | 7 | class Account { 8 | constructor(id, version, name, referenceId, accountNumber, created, updated) { 9 | this.id = id; 10 | this.version = version; 11 | this.name = name; 12 | this.referenceId = referenceId; 13 | this.accountNumber = accountNumber; 14 | this.created = created; 15 | this.updated = updated; 16 | } 17 | } 18 | 19 | const accounts = []; 20 | 21 | const accountRepository = { 22 | save: (account) => { 23 | // This simulates a save to the DB where the IDs get allocated 24 | let id = Math.floor(Math.random() * Math.floor(100000)); 25 | let accountNumber = Math.floor(Math.random() * Math.floor(100000)); 26 | let version = Math.floor(Math.random() * Math.floor(10)); 27 | account.id = id; 28 | account.version = version; 29 | account.accountNumber.id = accountNumber; 30 | accounts.push(account); 31 | return account; 32 | }, 33 | 34 | findByAccountNumber: async (accountNumber) => { 35 | return accounts.find( 36 | (account) => account.accountNumber.id == accountNumber 37 | ); 38 | }, 39 | }; 40 | 41 | module.exports = { 42 | Account, 43 | AccountNumber, 44 | accountRepository, 45 | }; 46 | -------------------------------------------------------------------------------- /examples/v3/provider-state-injected/provider/account-service.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { Verifier } = require('@pact-foundation/pact'); 3 | const { accountService } = require('./account-service'); 4 | const { 5 | Account, 6 | AccountNumber, 7 | accountRepository, 8 | } = require('./account-repository'); 9 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 10 | 11 | describe('Account Service', () => { 12 | let server; 13 | beforeAll(() => (server = accountService.listen(8081))); 14 | afterAll((done) => { 15 | console.log('closing server!'); 16 | server.close(done); 17 | }); 18 | 19 | it('validates the expectations of Transaction Service', () => { 20 | let opts = { 21 | // if the provider name is set, and we have PACT_BROKER_BASE_URL plus env var creds set 22 | // it will automatically attempt to retrieve from a pact broker via the default consumer version selectors. 23 | // if we are verifying a pact directory source, we do not need to add the provider name 24 | // as it is inferred from the pact file. 25 | // this doesn't feel like desirable behavior (we should only verify one source at a time!) 26 | // see https://github.com/pact-foundation/pact-reference/issues/250 27 | // provider: 'Account Service', 28 | providerBaseUrl: 'http://localhost:8081', 29 | logLevel: LOG_LEVEL, 30 | stateHandlers: { 31 | 'Account Test001 exists': { 32 | setup: (params) => { 33 | let account = new Account( 34 | 0, 35 | 0, 36 | 'Test001', 37 | params.accountRef, 38 | new AccountNumber(0), 39 | Date.now(), 40 | Date.now() 41 | ); 42 | let persistedAccount = accountRepository.save(account); 43 | return Promise.resolve({ 44 | accountNumber: persistedAccount.accountNumber.id, 45 | }); 46 | }, 47 | }, 48 | 'set id': { 49 | setup: (params) => Promise.resolve({ id: params.id }), 50 | }, 51 | 'set path': { 52 | setup: (params) => 53 | Promise.resolve({ id: params.id, path: params.path }), 54 | }, 55 | }, 56 | 57 | pactUrls: [ 58 | path.resolve( 59 | process.cwd(), 60 | './pacts/TransactionService-AccountService.json' 61 | ), 62 | ], 63 | }; 64 | 65 | return new Verifier(opts).verifyProvider(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "exit": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/README.md: -------------------------------------------------------------------------------- 1 | # run specific verifications example 2 | 3 | Using some pre-created pact files we are demonstrating the possibility to rerun specific verifications by filtering using env. variables. 4 | 5 | Main documentation: https://github.com/pact-foundation/pact-js/#re-run-specific-verification-failures 6 | 7 | This folder contains 3 pact files, each file contains some interactions that should be run, and some that should be skipped. Every test that should be skipped sends a `GET` requests to the `/fail` endpoint and expects `"result": "OK"`. If those interactions are executed they will fail (because the endpoint `/fail` does not return the expected result) and with it the whole test-run. That way these tests are also used as automated tests for pact-js itself and check if the filtering works correctly. To achieve that the env variables are set inside of `test/provider.spec.js` using `process.env.`. 8 | 9 | To play around with the filtering delete the `process.env. ...` lines in `test/provider.spec.js`, set the env. variables outside the test run e.g. `PACT_DESCRIPTION="a request to be skipped" npm run test:provider` and see what interactions are executed 10 | 11 | Run tests with `npm run test:provider` 12 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/filter-by-PACT_DESCRIPTION.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumer": { 3 | "name": "Filter Service V3" 4 | }, 5 | "interactions": [ 6 | { 7 | "description": "a request to be used", 8 | "providerStates": [ 9 | ], 10 | "request": { 11 | "method": "GET", 12 | "path": "/pass" 13 | }, 14 | "response": { 15 | "body": { 16 | "result": "OK" 17 | }, 18 | "headers": { 19 | "Content-Type": "application/json; charset=utf-8" 20 | }, 21 | "status": 200 22 | } 23 | }, 24 | { 25 | "description": "a request to be skipped", 26 | "providerStates": [ 27 | ], 28 | "request": { 29 | "method": "GET", 30 | "path": "/fail" 31 | }, 32 | "response": { 33 | "body": { 34 | "result": "OK" 35 | }, 36 | "headers": { 37 | "Content-Type": "application/json; charset=utf-8" 38 | }, 39 | "status": 200 40 | } 41 | }, 42 | { 43 | "description": "a request to be used because it's description is part of what is given in `PACT_DESCRIPTION`", 44 | "providerStates": [ 45 | ], 46 | "request": { 47 | "method": "GET", 48 | "path": "/pass" 49 | }, 50 | "response": { 51 | "body": { 52 | "result": "OK" 53 | }, 54 | "headers": { 55 | "Content-Type": "application/json; charset=utf-8" 56 | }, 57 | "status": 200 58 | } 59 | } 60 | ], 61 | "metadata": { 62 | "pactJs": { 63 | "version": "10.0.0-beta.35" 64 | }, 65 | "pactRust": { 66 | "version": "0.8.14" 67 | }, 68 | "pactSpecification": { 69 | "version": "3.0.0" 70 | } 71 | }, 72 | "provider": { 73 | "name": "filter-provider" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/filter-by-PACT_PROVIDER_NO_STATE.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumer": { 3 | "name": "Filter Service V3" 4 | }, 5 | "interactions": [ 6 | { 7 | "description": "a request to be used", 8 | "providerStates": [ 9 | ], 10 | "request": { 11 | "body": { 12 | }, 13 | "headers": { 14 | "Content-Type": "application/json" 15 | }, 16 | "method": "GET", 17 | "path": "/pass" 18 | }, 19 | "response": { 20 | "body": { 21 | "result": "OK" 22 | }, 23 | "headers": { 24 | "Content-Type": "application/json; charset=utf-8" 25 | }, 26 | "status": 200 27 | } 28 | }, 29 | { 30 | "description": "a request to be skipped", 31 | "providerStates": [ 32 | { 33 | "name": "state to be skipped", 34 | "params": { 35 | "some": "data" 36 | } 37 | } 38 | ], 39 | "request": { 40 | "body": { 41 | }, 42 | "headers": { 43 | "Content-Type": "application/json" 44 | }, 45 | "method": "GET", 46 | "path": "/fail" 47 | }, 48 | "response": { 49 | "body": { 50 | "result": "OK" 51 | }, 52 | "headers": { 53 | "Content-Type": "application/json; charset=utf-8" 54 | }, 55 | "status": 200 56 | } 57 | }, 58 | { 59 | "description": "a other request to be skipped", 60 | "providerStates": [ 61 | { 62 | "name": "state 1", 63 | "params": { 64 | "some": "data" 65 | } 66 | }, 67 | { 68 | "name": "state 2", 69 | "params": { 70 | "some": "data" 71 | } 72 | } 73 | ], 74 | "request": { 75 | "body": { 76 | }, 77 | "headers": { 78 | "Content-Type": "application/json" 79 | }, 80 | "method": "GET", 81 | "path": "/fail" 82 | }, 83 | "response": { 84 | "body": { 85 | "result": "OK" 86 | }, 87 | "headers": { 88 | "Content-Type": "application/json; charset=utf-8" 89 | }, 90 | "status": 200 91 | } 92 | }, 93 | { 94 | "description": "an other request to be used", 95 | "providerStates": [ 96 | ], 97 | "request": { 98 | "body": { 99 | }, 100 | "headers": { 101 | "Content-Type": "application/json" 102 | }, 103 | "method": "GET", 104 | "path": "/pass" 105 | }, 106 | "response": { 107 | "body": { 108 | "result": "OK" 109 | }, 110 | "headers": { 111 | "Content-Type": "application/json; charset=utf-8" 112 | }, 113 | "status": 200 114 | } 115 | } 116 | ], 117 | "metadata": { 118 | "pactJs": { 119 | "version": "10.0.0-beta.35" 120 | }, 121 | "pactRust": { 122 | "version": "0.8.14" 123 | }, 124 | "pactSpecification": { 125 | "version": "3.0.0" 126 | } 127 | }, 128 | "provider": { 129 | "name": "filter-provider" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/filter-by-PACT_PROVIDER_STATE.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumer": { 3 | "name": "Filter Service V3" 4 | }, 5 | "interactions": [ 6 | { 7 | "description": "a request to be used", 8 | "providerStates": [ 9 | { 10 | "name": "state to be used", 11 | "params": { 12 | "some": "data" 13 | } 14 | } 15 | ], 16 | "request": { 17 | "body": { 18 | }, 19 | "headers": { 20 | "Content-Type": "application/json" 21 | }, 22 | "method": "GET", 23 | "path": "/pass" 24 | }, 25 | "response": { 26 | "body": { 27 | "result": "OK" 28 | }, 29 | "headers": { 30 | "Content-Type": "application/json; charset=utf-8" 31 | }, 32 | "status": 200 33 | } 34 | }, 35 | { 36 | "description": "a request to be skipped", 37 | "providerStates": [ 38 | { 39 | "name": "state to be skipped", 40 | "params": { 41 | "some": "data" 42 | } 43 | } 44 | ], 45 | "request": { 46 | "body": { 47 | }, 48 | "headers": { 49 | "Content-Type": "application/json" 50 | }, 51 | "method": "GET", 52 | "path": "/fail" 53 | }, 54 | "response": { 55 | "body": { 56 | "result": "OK" 57 | }, 58 | "headers": { 59 | "Content-Type": "application/json; charset=utf-8" 60 | }, 61 | "status": 200 62 | } 63 | }, 64 | { 65 | "description": "a request to be skipped because there are no states", 66 | "providerStates": [ 67 | ], 68 | "request": { 69 | "body": { 70 | }, 71 | "headers": { 72 | "Content-Type": "application/json" 73 | }, 74 | "method": "GET", 75 | "path": "/fail" 76 | }, 77 | "response": { 78 | "body": { 79 | "result": "OK" 80 | }, 81 | "headers": { 82 | "Content-Type": "application/json; charset=utf-8" 83 | }, 84 | "status": 200 85 | } 86 | }, 87 | { 88 | "description": "a request to be used because one of its states is in `PACT_PROVIDER_STATE`", 89 | "providerStates": [ 90 | { 91 | "name": "state to be used", 92 | "params": { 93 | "some": "data" 94 | } 95 | } 96 | ], 97 | "request": { 98 | "body": { 99 | }, 100 | "headers": { 101 | "Content-Type": "application/json" 102 | }, 103 | "method": "GET", 104 | "path": "/pass" 105 | }, 106 | "response": { 107 | "body": { 108 | "result": "OK" 109 | }, 110 | "headers": { 111 | "Content-Type": "application/json; charset=utf-8" 112 | }, 113 | "status": 200 114 | } 115 | } 116 | ], 117 | "metadata": { 118 | "pactJs": { 119 | "version": "10.0.0-beta.35" 120 | }, 121 | "pactRust": { 122 | "version": "0.8.14" 123 | }, 124 | "pactSpecification": { 125 | "version": "3.0.0" 126 | } 127 | }, 128 | "provider": { 129 | "name": "filter-provider" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-specific-verifications", 3 | "version": "1.0.0", 4 | "description": "Pact JS E2E Example", 5 | "scripts": { 6 | "test": "npm run test:provider", 7 | "test:provider": "mocha test/provider.spec.js" 8 | }, 9 | "author": "artur@jankaritech.com", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@pact-foundation/pact": "file:../../../dist", 13 | "chai": "^4.3.6", 14 | "chai-as-promised": "^7.1.1", 15 | "mocha": "^10.0.0" 16 | }, 17 | "dependencies": { 18 | "body-parser": "^1.20.0", 19 | "cors": "^2.8.5", 20 | "express": "^4.18.1" 21 | }, 22 | "config": { 23 | "pact_do_not_track": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/provider.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const bodyParser = require('body-parser'); 4 | 5 | const server = express(); 6 | server.use(cors()); 7 | server.use(bodyParser.json()); 8 | server.use( 9 | bodyParser.urlencoded({ 10 | extended: true, 11 | }) 12 | ); 13 | server.use((req, res, next) => { 14 | res.header('Content-Type', 'application/json; charset=utf-8'); 15 | next(); 16 | }); 17 | 18 | server.get('/pass', (req, res) => { 19 | res.json({ result: 'OK' }); 20 | res.end(); 21 | }); 22 | server.get('/fail', (req, res) => { 23 | res.json({ result: 'FAIL' }); 24 | res.end(); 25 | }); 26 | 27 | module.exports = { 28 | server, 29 | }; 30 | -------------------------------------------------------------------------------- /examples/v3/run-specific-verifications/test/provider.spec.js: -------------------------------------------------------------------------------- 1 | const { Verifier } = require('@pact-foundation/pact'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | chai.use(chaiAsPromised); 5 | const { server } = require('../provider.js'); 6 | const path = require('path'); 7 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 8 | 9 | server.listen(8081, '127.0.0.1', () => { 10 | console.log('Service listening on http://127.0.0.1:8081'); 11 | }); 12 | 13 | // Verify that the provider meets all consumer expectations 14 | describe('Pact Verification', () => { 15 | it('filter by PACT_DESCRIPTION', () => { 16 | process.env.PACT_DESCRIPTION = 'a request to be used'; 17 | return new Verifier({ 18 | // provider: 'filter-provider', 19 | providerBaseUrl: 'http://127.0.0.1:8081', 20 | pactUrls: [ 21 | path.resolve(process.cwd(), './filter-by-PACT_DESCRIPTION.json'), 22 | ], 23 | logLevel: LOG_LEVEL, 24 | }) 25 | .verifyProvider() 26 | .then((output) => { 27 | console.log('Pact Verification Complete!'); 28 | console.log('Result:', output); 29 | }); 30 | }); 31 | it('filter by PACT_PROVIDER_STATE', () => { 32 | process.env.PACT_PROVIDER_STATE = 'a state to be used'; 33 | return new Verifier({ 34 | // if the provider name is set, and we have PACT_BROKER_BASE_URL plus env var creds set 35 | // it will automatically attempt to retrieve from a pact broker via the default consumer version selectors. 36 | // if we are verifying a pact directory source, we do not need to add the provider name 37 | // as it is inferred from the pact file. 38 | 39 | // provider: 'filter-provider', 40 | providerBaseUrl: 'http://127.0.0.1:8081', 41 | pactUrls: [ 42 | path.resolve(process.cwd(), './filter-by-PACT_PROVIDER_STATE.json'), 43 | ], 44 | }) 45 | .verifyProvider() 46 | .then((output) => { 47 | console.log('Pact Verification Complete!'); 48 | console.log('Result:', output); 49 | }); 50 | }); 51 | it('filter by PACT_PROVIDER_NO_STATE', () => { 52 | process.env.PACT_PROVIDER_NO_STATE = 'TRUE'; 53 | return new Verifier({ 54 | // provider: 'filter-provider', 55 | providerBaseUrl: 'http://127.0.0.1:8081', 56 | pactUrls: [ 57 | path.resolve(process.cwd(), './filter-by-PACT_PROVIDER_NO_STATE.json'), 58 | ], 59 | }) 60 | .verifyProvider() 61 | .then((output) => { 62 | console.log('Pact Verification Complete!'); 63 | console.log('Result:', output); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /examples/v3/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | for i in *; do 6 | if [[ -d $i ]]; then 7 | echo ------------------------------------------------- 8 | echo ---- $i 9 | echo ------------------------------------------------- 10 | pushd "$i" 11 | npm i 12 | rm -rf "node_modules/@pact-foundation/pact" 13 | echo "linking pact" 14 | npm link @pact-foundation/pact 15 | npm t 16 | popd 17 | fi 18 | done 19 | -------------------------------------------------------------------------------- /examples/v3/todo-consumer/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "exit": true 5 | } 6 | -------------------------------------------------------------------------------- /examples/v3/todo-consumer/README.md: -------------------------------------------------------------------------------- 1 | ## Example Consumer test using Pact V3 features 2 | 3 | This is an example project with a test that uses V3 Pact features. It has an example test for both JSON and XML format. 4 | 5 | ## To run it 6 | 7 | 1. Install the project dependencies 8 | 9 | ```console 10 | cd examples/v3/todo-consumer/ 11 | npm install 12 | ``` 13 | 14 | 2. Run the test with Mocha 15 | 16 | ```console 17 | npm test 18 | ``` 19 | 20 | ## V3 features 21 | 22 | This has 2 tests. The first uses generators and matchers for numbers and datetime values. The second test deals with XML responses. -------------------------------------------------------------------------------- /examples/v3/todo-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-consumer", 3 | "version": "0.0.0", 4 | "description": "Node Todo Consumer", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "test": "npm run test:consumer && npm run test:provider", 9 | "test:consumer": "mocha test/consumer.spec.js", 10 | "test:provider": "mocha test/provider.spec.js" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "config": { 15 | "pact_do_not_track": true 16 | }, 17 | "dependencies": { 18 | "axios": "^1.8.4", 19 | "express": "^4.18.1", 20 | "eyes": "^0.1.8", 21 | "fast-xml-parser": "^4.4.1", 22 | "ramda": "^0.28.0" 23 | }, 24 | "devDependencies": { 25 | "@pact-foundation/pact": "file:../../../dist", 26 | "chai": "^4.3.6", 27 | "chai-as-promised": "^7.1.1", 28 | "mocha": "^10.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/v3/todo-consumer/provider.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const server = express(); 3 | 4 | server.get('/projects', (req, res) => { 5 | if (req.headers.accept.endsWith('xml')) { 6 | res.header('Content-Type', 'application/todo+xml'); 7 | res.send( 8 | "" 9 | ); 10 | } else { 11 | res.header('Content-Type', 'application/json'); 12 | res.send([ 13 | { 14 | id: 1, 15 | name: 'Project 1', 16 | type: 'activity', 17 | due: '2016-02-11T09:46:56.023Z', 18 | tasks: [ 19 | { 20 | done: true, 21 | id: 1, 22 | name: 'Task 1', 23 | }, 24 | { 25 | done: true, 26 | id: 2, 27 | name: 'Task 2', 28 | }, 29 | { 30 | done: true, 31 | id: 3, 32 | name: 'Task 3', 33 | }, 34 | { 35 | done: true, 36 | id: 4, 37 | name: 'Task 4', 38 | }, 39 | ], 40 | }, 41 | ]); 42 | } 43 | }); 44 | 45 | server.post('/projects/:id/images', (req, res) => { 46 | res.status(201).end(); 47 | }); 48 | 49 | module.exports = { 50 | server, 51 | }; 52 | -------------------------------------------------------------------------------- /examples/v3/todo-consumer/providerService.js: -------------------------------------------------------------------------------- 1 | const { server } = require('./provider.js'); 2 | 3 | server.listen(8081, () => { 4 | console.log('SOAP Service listening on http://localhost:8081'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/v3/todo-consumer/src/todo.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { XMLParser } = require('fast-xml-parser'); 3 | const eyes = require('eyes'); 4 | const R = require('ramda'); 5 | const fs = require('fs'); 6 | const parser = new XMLParser({ 7 | ignoreAttributes: false, 8 | }); 9 | let serverUrl = 'http://127.0.0.1:2203'; 10 | 11 | module.exports = { 12 | getProjects: async (format = 'json') => { 13 | return axios 14 | .get(serverUrl + '/projects?from=today', { 15 | headers: { 16 | Accept: 'application/' + format, 17 | }, 18 | }) 19 | .then((response) => { 20 | console.log('todo response:'); 21 | eyes.inspect(response.data); 22 | if (format.endsWith('xml')) { 23 | const result = parser.parse(response.data); 24 | console.dir(result, { depth: 10 }); 25 | console.log(R.path(['ns1:projects'], result)); 26 | return R.path(['ns1:projects'], result); 27 | } 28 | return response.data; 29 | }) 30 | .catch((error) => { 31 | console.log('todo error', error.message); 32 | return Promise.reject(error); 33 | }); 34 | }, 35 | setUrl: function (url) { 36 | serverUrl = url; 37 | return this; 38 | }, 39 | postImage: (id, image) => { 40 | const data = fs.readFileSync(image); 41 | return axios.post(serverUrl + '/projects/' + id + '/images', data, { 42 | headers: { 43 | 'Content-Type': 'image/jpeg', 44 | }, 45 | }); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /examples/v3/todo-consumer/test/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-js/9aeb7a443796cb0cb4e42b1472a0496e3749e12d/examples/v3/todo-consumer/test/example.jpg -------------------------------------------------------------------------------- /examples/v3/todo-consumer/test/provider.spec.js: -------------------------------------------------------------------------------- 1 | const { PactV3, MatchersV3, XmlBuilder } = require('@pact-foundation/pact'); 2 | const { Verifier } = require('@pact-foundation/pact'); 3 | const chai = require('chai'); 4 | const chaiAsPromised = require('chai-as-promised'); 5 | chai.use(chaiAsPromised); 6 | const { server } = require('../provider.js'); 7 | const path = require('path'); 8 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 9 | 10 | server.listen(8081, () => { 11 | console.log('SOAP API listening on http://localhost:8081'); 12 | }); 13 | 14 | describe('Pact XML Verification', () => { 15 | it('validates the expectations of Matching Service', () => { 16 | const opts = { 17 | // provider: 'XML Service', 18 | providerBaseUrl: 'http://localhost:8081', 19 | pactUrls: ['./pacts/TodoApp-TodoServiceV3.json'], 20 | stateHandlers: { 21 | 'i have a list of projects': (params) => {}, 22 | 'i have a project': (params) => {}, 23 | }, 24 | logLevel: LOG_LEVEL, 25 | }; 26 | 27 | return new Verifier(opts).verifyProvider().then((output) => { 28 | console.log('Pact Verification Complete!'); 29 | console.log(output); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/v3/typescript/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "require": ["tsx", "source-map-support/register"], 5 | "exit": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/v3/typescript/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosPromise } from 'axios'; 2 | 3 | export class UserService { 4 | constructor(private url: string) {} 5 | 6 | public getUser = (id: number): AxiosPromise => { 7 | return axios.request({ 8 | baseURL: this.url, 9 | headers: { Accept: 'application/json' }, 10 | method: 'GET', 11 | url: `/users/${id}`, 12 | }); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /examples/v3/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-pact-v3", 3 | "version": "1.0.0", 4 | "description": "TypeScript Pact Example (Using V3 version)", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/*.spec.ts", 8 | "mocha:direct": "mocha" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@pact-foundation/pact": "file:../../../dist", 14 | "@types/chai": "^4.3.3", 15 | "@types/chai-as-promised": "7.1.5", 16 | "@types/mocha": "^9.1.1", 17 | "@types/node": "^18.7.11", 18 | "@types/sinon-chai": "^3.2.8", 19 | "chai": "^4.3.6", 20 | "chai-as-promised": "^7.1.1", 21 | "mocha": "^10.0.0", 22 | "sinon": "^14.0.0", 23 | "sinon-chai": "^3.7.0", 24 | "tslint": "^5.20.1", 25 | "tslint-config-prettier": "^1.18.0", 26 | "tsx": "^4.19.2", 27 | "typescript": "^4.7.4" 28 | }, 29 | "dependencies": { 30 | "axios": "^1.8.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/v3/typescript/test/user.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import sinonChai from 'sinon-chai'; 4 | import { PactV3, MatchersV3, LogLevel } from '@pact-foundation/pact'; 5 | import { UserService } from '../index'; 6 | const { like } = MatchersV3; 7 | const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE'; 8 | 9 | const expect = chai.expect; 10 | 11 | chai.use(sinonChai); 12 | chai.use(chaiAsPromised); 13 | 14 | describe('The Users API', () => { 15 | let userService: UserService; 16 | 17 | // Setup the 'pact' between two applications 18 | const provider = new PactV3({ 19 | consumer: 'User Web', 20 | provider: 'User API', 21 | logLevel: LOG_LEVEL as LogLevel, 22 | }); 23 | const userExample = { id: 1, name: 'Homer Simpson' }; 24 | const EXPECTED_BODY = like(userExample); 25 | 26 | describe('get /users/:id', () => { 27 | it('returns the requested user', () => { 28 | // Arrange 29 | provider 30 | .given('a user with ID 1 exists') 31 | .uponReceiving('a request to get a user') 32 | .withRequest({ 33 | method: 'GET', 34 | path: '/users/1', 35 | }) 36 | .willRespondWith({ 37 | status: 200, 38 | headers: { 'content-type': 'application/json' }, 39 | body: EXPECTED_BODY, 40 | }); 41 | 42 | return provider.executeTest(async (mockserver) => { 43 | // Act 44 | userService = new UserService(mockserver.url); 45 | const response = await userService.getUser(1); 46 | 47 | // Assert 48 | expect(response.data).to.deep.eq(userExample); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/v3/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "noLib": false, 5 | "noImplicitReturns": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "emitDecoratorMetadata": true, 12 | "declaration": true, 13 | "experimentalDecorators": true, 14 | "target": "es5", 15 | "lib": ["es2016", "dom", "esnext.asynciterable"] 16 | }, 17 | "include": ["src"], 18 | "exclude": ["./node_modules/**"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/v3/typescript/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "interface-name": [true, "never-prefix"], 7 | "no-var-requires": false, 8 | "ordered-imports": false, 9 | "semicolon": [true, "never"], 10 | "whitespace": [ 11 | true, 12 | "check-branch", 13 | "check-decl", 14 | "check-operator", 15 | "check-module", 16 | "check-separator", 17 | "check-rest-spread", 18 | "check-type", 19 | "check-typecast", 20 | "check-type-operator", 21 | "check-preblock" 22 | ] 23 | }, 24 | "rulesDirectory": [] 25 | } 26 | -------------------------------------------------------------------------------- /examples/v4/matchers/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "require": ["tsx"], 5 | "exit": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/v4/matchers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v4matchers", 3 | "version": "1.0.0", 4 | "description": "Pact JS V4 Matchers example", 5 | "scripts": { 6 | "clean": "rimraf pacts", 7 | "pretest": "npm run clean", 8 | "test": "npm run test:consumer && npm run test:provider", 9 | "test:consumer": "mocha consumer.spec.ts", 10 | "test:provider": "mocha provider.spec.ts" 11 | }, 12 | "author": "matt.fellows@onegeek.com.au", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@pact-foundation/pact": "file:../../../dist", 16 | "@types/chai": "^4.3.3", 17 | "@types/chai-as-promised": "7.1.5", 18 | "@types/mocha": "^9.1.1", 19 | "axios": "^1.8.4", 20 | "chai": "^3.5.0", 21 | "chai-as-promised": "^7.1.1", 22 | "express": "^4.18.1", 23 | "mocha": "^10.0.0", 24 | "rimraf": "^3.0.2", 25 | "tsx": "^4.19.2", 26 | "typescript": "^4.7.4" 27 | }, 28 | "config": { 29 | "pact_do_not_track": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/v4/matchers/provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel, Verifier } from '@pact-foundation/pact'; 2 | import { after } from 'mocha'; 3 | 4 | const express = require('express'); 5 | const server = express(); 6 | 7 | server.use((_, res, next) => { 8 | res.header('Content-Type', 'application/json; charset=utf-8'); 9 | next(); 10 | }); 11 | 12 | server.get('/status', (req, res) => { 13 | res.send(202); 14 | }); 15 | 16 | server.get('/eachKeyMatches', (req, res) => { 17 | res.json({ 18 | // '1': 'foo', // <- fails, is a number 19 | // 'not valid': '1', // <- fails, is a string but does not match regex (has space) 20 | // key: 'value', // <- fails, key does not match regex (no trailing number) 21 | key1: 'value', 22 | key2: 'value', 23 | }); 24 | }); 25 | 26 | server.get('/eachValueMatches', (req, res) => { 27 | res.json({ 28 | // foo: 1, // <- fails, is a number 29 | // foo: '1', // <- fails, is a string but does not match regex 30 | for: 'string with space', 31 | }); 32 | }); 33 | 34 | const app = server.listen(8080, () => { 35 | console.log('API listening on http://localhost:8080'); 36 | }); 37 | 38 | describe('V4 Matchers', () => { 39 | describe('eachKeyLike', () => { 40 | const pact = new Verifier({ 41 | pactUrls: ['./pacts/myconsumer-myprovider.json'], 42 | providerBaseUrl: 'http://localhost:8080', 43 | logLevel: (process.env.LOG_LEVEL as LogLevel) || 'trace', 44 | }); 45 | it('verifies the pact', () => pact.verifyProvider()); 46 | }); 47 | 48 | after(() => { 49 | app.close(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/v4/plugins/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 60000, 3 | "recursive": true, 4 | "require": ["tsx"], 5 | "exit": true 6 | } 7 | -------------------------------------------------------------------------------- /examples/v4/plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "description": "Pact JS E2E Example", 5 | "scripts": { 6 | "clean": "rimraf pacts", 7 | "pretest": "npm run clean", 8 | "test": "npm run test:consumer && npm run test:provider", 9 | "test:consumer": "mocha test/*.consumer.spec.ts", 10 | "test:provider": "mocha test/*.provider.spec.ts" 11 | }, 12 | "author": "matt.fellows@onegeek.com.au", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@pact-foundation/pact": "file:../../../dist", 16 | "@types/chai": "^4.3.11", 17 | "@types/chai-as-promised": "7.1.8", 18 | "@types/mocha": "^10.0.6", 19 | "axios": "^1.8.4", 20 | "chai": "^3.5.0", 21 | "chai-as-promised": "^7.1.1", 22 | "express": "^4.18.2", 23 | "mocha": "^10.2.0", 24 | "rimraf": "^5.0.5", 25 | "tsx": "^4.19.2", 26 | "typescript": "^5.3.3" 27 | }, 28 | "config": { 29 | "pact_do_not_track": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/v4/plugins/protocol.ts: -------------------------------------------------------------------------------- 1 | export const parseMattMessage = (raw: string): string => { 2 | return raw.replace(/(MATT)+/g, '').trim(); 3 | }; 4 | export const generateMattMessage = (raw: string): string => { 5 | return `MATT${raw}MATT`; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/v4/plugins/provider.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-expression no-empty */ 2 | import net = require('net'); 3 | import express = require('express'); 4 | import * as http from 'http'; 5 | import { generateMattMessage, parseMattMessage } from './protocol'; 6 | 7 | export const startTCPServer = ( 8 | host: string, 9 | port?: number 10 | ): Promise => { 11 | const server = net.createServer(); 12 | 13 | server.on('connection', (sock) => { 14 | sock.on('error', (err) => { 15 | console.log(`received TCP socket error: ${err}. Error will be ignored`); 16 | }); 17 | sock.on('data', (data) => { 18 | const msg = parseMattMessage(data.toString()); 19 | 20 | if (msg === 'hellotcp') { 21 | sock.write(generateMattMessage('tcpworld')); 22 | } else { 23 | sock.write(generateMattMessage('message not understood')); 24 | } 25 | sock.write('\n'); 26 | sock.end(); 27 | }); 28 | }); 29 | 30 | return new Promise((resolve) => { 31 | server.listen(port, host, () => { 32 | console.log('listening on part'); 33 | }); 34 | 35 | server.on('error', (err) => { 36 | console.log(`received TCP server error: ${err}. Error will be ignored`); 37 | }); 38 | 39 | server.on('listening', () => { 40 | resolve((server.address() as net.AddressInfo).port); 41 | }); 42 | }); 43 | }; 44 | 45 | export const startHTTPServer = ( 46 | host: string, 47 | port?: number 48 | ): Promise => { 49 | const server: express.Express = express(); 50 | 51 | server.post('/matt', (_, res) => { 52 | res.setHeader('content-type', 'application/matt'); 53 | res.send(generateMattMessage('world')); 54 | }); 55 | 56 | server.on('error', (err) => { 57 | console.log(`received HTTP error: ${err}. Error will be ignored`); 58 | }); 59 | 60 | let s: http.Server; 61 | return new Promise((resolve) => { 62 | s = server.listen(port ? port : 0, host, () => { 63 | resolve(); 64 | }); 65 | }).then(() => s); 66 | }; 67 | -------------------------------------------------------------------------------- /examples/v4/plugins/test/matt.provider.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-expression no-empty */ 2 | import { Verifier } from '@pact-foundation/pact'; 3 | import { AddressInfo } from 'net'; 4 | import path = require('path'); 5 | import { startHTTPServer, startTCPServer } from '../provider'; 6 | 7 | describe('Plugins', () => { 8 | const HOST = '127.0.0.1'; 9 | 10 | describe('Verification', () => { 11 | describe('with MATT protocol', () => { 12 | let httpPort: number; 13 | let tcpPort: number; 14 | 15 | beforeEach(async () => { 16 | httpPort = ((await startHTTPServer(HOST)).address() as AddressInfo) 17 | .port; 18 | tcpPort = await startTCPServer(HOST); 19 | 20 | console.log('Started on ports TCP: ', tcpPort, ' HTTP:', httpPort); 21 | }); 22 | 23 | it('validates TCP and HTTP matt messages', async () => { 24 | const v = new Verifier({ 25 | providerBaseUrl: `http://${HOST}:${httpPort}`, 26 | transports: [ 27 | { 28 | port: tcpPort, 29 | protocol: 'matt', 30 | scheme: 'tcp', 31 | }, 32 | ], 33 | pactUrls: [ 34 | path.join(__dirname, '../', 'pacts', 'myconsumer-myprovider.json'), 35 | ], 36 | }); 37 | 38 | return v.verifyProvider(); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/v4/plugins/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "noLib": false, 5 | "noImplicitReturns": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "emitDecoratorMetadata": true, 12 | "declaration": true, 13 | "esModuleInterop": true, 14 | "experimentalDecorators": true, 15 | "target": "es5", 16 | "lib": ["es2016", "dom", "esnext.asynciterable"] 17 | }, 18 | "exclude": ["./node_modules/**"] 19 | } 20 | -------------------------------------------------------------------------------- /scripts/ci/build-and-test.ps1: -------------------------------------------------------------------------------- 1 | npm ci 2 | npm run dist 3 | npm run build:v3 4 | Copy-Item "package.json" -Destination "dist" 5 | Copy-Item "package-lock.json" -Destination "dist" 6 | Copy-Item -Path "native" -Destination "dist" -Recurse 7 | pushd dist 8 | npm link 9 | popd 10 | 11 | Push-Location dist 12 | npm link 13 | Pop-Location 14 | 15 | Get-ChildItem ".\examples" -Directory | ForEach-Object { 16 | if ($_.Name -ne "v3") { 17 | Write-Output "Running examples in $($_.Name)" 18 | pushd $_.FullName 19 | npm i 20 | npm link @pact-foundation/pact 21 | npm t 22 | if ($LastExitCode -ne 0) { 23 | Write-Output "Non-zero exit code!" 24 | $host.SetShouldExit($LastExitCode) 25 | } 26 | popd 27 | } 28 | } 29 | 30 | Write-Output "Done with V2 tests" 31 | 32 | Get-ChildItem ".\examples\v3" -Directory | ForEach-Object { 33 | Write-Output "Running V3 examples in $($_.Name)" 34 | pushd $_.FullName 35 | npm i 36 | Remove-Item -LiteralPath "node_modules\@pact-foundation\pact" -Force -Recurse 37 | 38 | npm link @pact-foundation/pact 39 | npm t 40 | if ($LastExitCode -ne 0) { 41 | Write-Output "Non-zero exit code!" 42 | $host.SetShouldExit($LastExitCode) 43 | } 44 | popd 45 | } 46 | 47 | Write-Output "Done with V3 tests" 48 | 49 | Get-ChildItem ".\examples\v4" -Directory | ForEach-Object { 50 | Write-Output "Running V4 examples in $($_.Name)" 51 | pushd $_.FullName 52 | npm i 53 | Remove-Item -LiteralPath "node_modules\@pact-foundation\pact" -Force -Recurse 54 | 55 | npm link @pact-foundation/pact 56 | npm t 57 | if ($LastExitCode -ne 0) { 58 | Write-Output "Non-zero exit code!" 59 | $host.SetShouldExit($LastExitCode) 60 | } 61 | popd 62 | } 63 | 64 | Write-Output "Done with V4 tests" 65 | -------------------------------------------------------------------------------- /scripts/ci/build-and-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | set -e 3 | set -u 4 | set -x 5 | 6 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 7 | . "$SCRIPT_DIR"/lib/robust-bash.sh 8 | 9 | npm ci 10 | 11 | npm run dist 12 | cp package.json ./dist 13 | 14 | export GIT_BRANCH=${GITHUB_HEAD_REF:-${GIT_REF#refs/heads/}} 15 | 16 | "${SCRIPT_DIR}"/lib/prepare-release.sh 17 | 18 | echo "--> Running coverage checks" 19 | npm run coverage 20 | 21 | cp package-lock.json dist 22 | cp -r scripts dist 23 | echo "This will be version '$(npx --yes absolute-version)'" 24 | 25 | if [ x"${SKIP_EXAMPLES:-}" == "x" ]; then 26 | echo "running all examples as SKIP_EXAMPLES not set" 27 | "${SCRIPT_DIR}"/test-examples.sh 28 | else 29 | echo "skipping examples as SKIP_EXAMPLES set" 30 | fi -------------------------------------------------------------------------------- /scripts/ci/lib/create_npmrc_file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 3 | . "$SCRIPT_DIR"/robust-bash.sh 4 | 5 | require_env_var NODE_AUTH_TOKEN 6 | 7 | set +x #Don't echo the NPM key 8 | 9 | NPMRC_FILE=.npmrc 10 | echo "@pact-foundation:registry=https://registry.npmjs.org/" > $NPMRC_FILE 11 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> $NPMRC_FILE 12 | echo "//registry.npmjs.org/:username=pact-foundation" >> $NPMRC_FILE 13 | echo "//registry.npmjs.org/:email=pact-foundation@googlegroups.com" >> $NPMRC_FILE 14 | echo "//registry.npmjs.org/:always-auth=true" >> $NPMRC_FILE 15 | 16 | set -x 17 | -------------------------------------------------------------------------------- /scripts/ci/lib/get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 3 | . "$SCRIPT_DIR"/robust-bash.sh 4 | 5 | require_binary grep 6 | 7 | VERSION="$(grep '\"version\"' package.json | grep -E -o "([0-9\.]+(-[a-z\.0-9]+)?)")" 8 | echo "$VERSION" -------------------------------------------------------------------------------- /scripts/ci/lib/prepare-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 3 | . "$SCRIPT_DIR"/robust-bash.sh 4 | 5 | require_binary grep 6 | require_binary sed 7 | require_binary find 8 | 9 | VERSION="$("$SCRIPT_DIR/get-version.sh")" 10 | 11 | echo "--> Preparing release version ${VERSION}" 12 | 13 | echo "--> Copy key artifacts into pact distribution" 14 | artifacts=(LICENSE *md package.json) 15 | 16 | for artifact in "${artifacts[@]}"; do 17 | echo " Copying ${artifact} => ./dist/${artifact}" 18 | cp "${artifact}" "./dist/${artifact}" 19 | done 20 | -------------------------------------------------------------------------------- /scripts/ci/lib/publish-9x.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 3 | . "$SCRIPT_DIR"/robust-bash.sh 4 | 5 | require_binary npm 6 | 7 | "${SCRIPT_DIR}"/prepare-release.sh 8 | VERSION="$("$SCRIPT_DIR/get-version.sh")" 9 | 10 | echo "--> Preparing npmrc file" 11 | "$SCRIPT_DIR"/create_npmrc_file.sh 12 | 13 | echo "--> Releasing version ${VERSION}" 14 | 15 | echo "--> Releasing artifacts" 16 | echo " Publishing pact@${VERSION}..." 17 | npm publish dist --access public 18 | echo " done!" 19 | -------------------------------------------------------------------------------- /scripts/ci/lib/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 3 | . "$SCRIPT_DIR"/robust-bash.sh 4 | 5 | require_binary npm 6 | 7 | "${SCRIPT_DIR}"/prepare-release.sh 8 | VERSION="$("$SCRIPT_DIR/get-version.sh")" 9 | 10 | echo "--> Preparing npmrc file" 11 | "$SCRIPT_DIR"/create_npmrc_file.sh 12 | 13 | echo "--> Releasing version ${VERSION}" 14 | echo "--> Releasing artifacts" 15 | echo " Publishing pact@${VERSION}..." 16 | npm publish ./dist --access public --tag latest 17 | echo " done!" -------------------------------------------------------------------------------- /scripts/ci/lib/robust-bash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | if [ -z "${LIB_ROBUST_BASH_SH:-}" ]; then 3 | LIB_ROBUST_BASH_SH=included 4 | 5 | function error { 6 | echo "❌ ${1:-}" 7 | } 8 | 9 | function log { 10 | echo "🔵 ${1:-}" 11 | } 12 | 13 | # Check to see that we have a required binary on the path 14 | function require_binary { 15 | if [ -z "${1:-}" ]; then 16 | error "${FUNCNAME[0]} requires an argument" 17 | exit 1 18 | fi 19 | 20 | if ! [ -x "$(command -v "$1")" ]; then 21 | error "The required executable '$1' is not on the path." 22 | exit 1 23 | fi 24 | } 25 | 26 | function require_env_var { 27 | var_name="${1:-}" 28 | if [ -z "${!var_name:-}" ]; then 29 | error "The required environment variable ${var_name} is empty" 30 | if [ ! -z "${2:-}" ]; then 31 | echo " - $2" 32 | fi 33 | exit 1 34 | fi 35 | } 36 | fi -------------------------------------------------------------------------------- /scripts/ci/release-9x.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -u 4 | 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 6 | . "$SCRIPT_DIR"/lib/robust-bash.sh 7 | 8 | require_env_var CI "This script must be run from CI. If you are running locally, note that it stamps your repo git settings." 9 | require_env_var GITHUB_ACTOR 10 | require_env_var NODE_AUTH_TOKEN 11 | 12 | # Setup git for github actions 13 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 14 | git config user.name "${GITHUB_ACTOR}" 15 | 16 | # It's easier to read the release notes 17 | # from the commit-and-tag-version tool before it runs 18 | RELEASE_NOTES="$(npx commit-and-tag-version --dry-run | awk 'BEGIN { flag=0 } /^---$/ { if (flag == 0) { flag=1 } else { flag=2 }; next } flag == 1')" 19 | # Don't release if there are no changes 20 | if [ "$(echo "$RELEASE_NOTES" | wc -l)" -eq 1 ] ; then 21 | echo "ERROR: This release would have no release notes. Does it include changes?" 22 | echo " - You must have at least one fix / feat commit to generate release notes" 23 | echo "*** STOPPING RELEASE PROCESS ***" 24 | exit 1 25 | fi 26 | # This is github actions' method for emitting multi-line values 27 | RELEASE_NOTES="${RELEASE_NOTES//'%'/'%25'}" 28 | RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" 29 | RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" 30 | echo "::set-output name=notes::$RELEASE_NOTES" 31 | 32 | npm ci 33 | "$SCRIPT_DIR"/build-and-test.sh 34 | npm run release 35 | 36 | # Emit version to next step 37 | VERSION="$("$SCRIPT_DIR/lib/get-version.sh")" 38 | echo "::set-output name=version::$VERSION" 39 | 40 | "$SCRIPT_DIR"/lib/publish-9x.sh 41 | 42 | # Push the new commit back to the repo. 43 | git push --follow-tags 44 | -------------------------------------------------------------------------------- /scripts/ci/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -u 4 | 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 6 | . "$SCRIPT_DIR"/lib/robust-bash.sh 7 | 8 | require_env_var CI "This script must be run from CI. If you are running locally, note that it stamps your repo git settings." 9 | require_env_var GITHUB_ACTOR 10 | require_env_var NODE_AUTH_TOKEN 11 | 12 | # Setup git for github actions 13 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 14 | git config user.name "${GITHUB_ACTOR}" 15 | 16 | # It's easier to read the release notes 17 | # from the commit-and-tag-version tool before it runs 18 | RELEASE_NOTES="$(npx commit-and-tag-version --dry-run | awk 'BEGIN { flag=0 } /^---$/ { if (flag == 0) { flag=1 } else { flag=2 }; next } flag == 1')" 19 | # Don't release if there are no changes 20 | if [ "$(echo "$RELEASE_NOTES" | wc -l)" -eq 1 ] ; then 21 | echo "ERROR: This release would have no release notes. Does it include changes?" 22 | echo " - You must have at least one fix / feat commit to generate release notes" 23 | echo "*** STOPPING RELEASE PROCESS ***" 24 | exit 1 25 | fi 26 | # This is github actions' method for emitting multi-line values 27 | RELEASE_NOTES="${RELEASE_NOTES//'%'/'%25'}" 28 | RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" 29 | RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" 30 | echo "::set-output name=notes::$RELEASE_NOTES" 31 | 32 | npm ci 33 | "$SCRIPT_DIR"/build-and-test.sh 34 | 35 | npm run release 36 | 37 | # Emit version to next step 38 | VERSION="$("$SCRIPT_DIR/lib/get-version.sh")" 39 | echo "::set-output name=version::$VERSION" 40 | 41 | "$SCRIPT_DIR"/lib/publish.sh 42 | 43 | # Push the new commit back to the repo. 44 | git push --follow-tags 45 | -------------------------------------------------------------------------------- /scripts/ci/test-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | set -e 3 | set -u 4 | set -x 5 | 6 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" # Figure out where the script is running 7 | . "$SCRIPT_DIR"/lib/robust-bash.sh 8 | 9 | # Link the build so that the examples are always testing the 10 | # current build, in it's properly exported format 11 | (cd dist && npm ci) 12 | 13 | echo "Running e2e examples build for node version $(node --version)" 14 | for i in examples/*; do 15 | [ -d "$i" ] || continue # prevent failure if not a directory 16 | [ -e "$i" ] || continue # prevent failure if there are no examples 17 | echo "--> running tests for: $i" 18 | pushd "$i" 19 | # replace pact dependency with locally build version 20 | contents="$(jq '.devDependencies."@pact-foundation/pact" = "file:../../dist"' package.json)" && \ 21 | echo "${contents}" > package.json 22 | 23 | if [ x"${SETUP_DIST_ONLY:-}" == "x" ]; then 24 | echo "running all examples as SETUP_DIST_ONLY not set" 25 | # npm ci does not work because we have just changed the package.json file 26 | npm install 27 | if [[ -f /.dockerenv && "$(uname -sm)" == 'Linux aarch64' && "$GITHUB_ACTIONS" == 'true' ]]; then 28 | npm run test:no:publish || npm test 29 | else 30 | npm test 31 | fi 32 | else 33 | echo "skipping testing of examples as SETUP_DIST_ONLY set" 34 | fi 35 | popd 36 | done 37 | 38 | echo "Running Vx examples build" 39 | 40 | # Commented because: 41 | # 1. We can't run the broker on windows CI 42 | # 2. We use the live broker in the v3 examples now anyway 43 | # docker pull pactfoundation/pact-broker 44 | # BROKER_ID=$(docker run -e PACT_BROKER_DATABASE_ADAPTER=sqlite -d -p 9292:9292 pactfoundation/pact-broker) 45 | 46 | # trap "docker kill $BROKER_ID" EXIT 47 | 48 | for i in examples/v*/*; do 49 | [ -d "$i" ] || continue # prevent failure if not a directory 50 | [ -e "$i" ] || continue # prevent failure if there are no examples 51 | echo "------------------------------------------------" 52 | echo "------------> continuing to test V3/v$ example project: $i" 53 | node --version 54 | pushd "$i" 55 | # replace pact dependency with locally build version 56 | contents="$(jq '.devDependencies."@pact-foundation/pact" = "file:../../../dist"' package.json)" && \ 57 | echo "${contents}" > package.json 58 | # npm ci does not work because we have just changed the package.json file 59 | if [ x"${SETUP_DIST_ONLY:-}" == "x" ]; then 60 | echo "running all examples as SETUP_DIST_ONLY not set" 61 | # npm ci does not work because we have just changed the package.json file 62 | npm install 63 | if [[ -f /.dockerenv && "$(uname -sm)" == 'Linux aarch64' && "$GITHUB_ACTIONS" == 'true' ]]; then 64 | npm run test:no:publish || npm test 65 | else 66 | npm test 67 | fi 68 | else 69 | echo "skipping testing of examples as SETUP_DIST_ONLY set" 70 | fi 71 | popd 72 | done -------------------------------------------------------------------------------- /scripts/deploy.ps1: -------------------------------------------------------------------------------- 1 | Set-PSDebug -Trace 1 2 | dir env: 3 | 4 | if ($env:APPVEYOR_REPO_TAG -eq "true") { 5 | Write-Output "Running deploy (APPVEYOR_REPO_TAG) is $env:APPVEYOR_REPO_TAG" 6 | npm install node-pre-gyp node-pre-gyp-github 7 | npm run build:v3 8 | Remove-Item 'native\target' -Recurse -Force 9 | npm run upload-binary 10 | } else { 11 | Write-Output "Skipping deploy (APPVEYOR_REPO_TAG) is $env:APPVEYOR_REPO_TAG" 12 | } 13 | -------------------------------------------------------------------------------- /scripts/install-plugins: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Usage: 4 | # $ curl -fsSL https://raw.githubusercontent.com/pact-foundation/pact-plugins/master/install-cli.sh | bash 5 | # or 6 | # $ wget -q https://raw.githubusercontent.com/pact-foundation/pact-plugins/master/install-cli.sh -O- | bash 7 | 8 | # While most shells support `local`, it technically isn't POSIX. This will check 9 | # for `local` and alias it to `typeset` if it doesn't exist. 10 | # shellcheck disable=SC3043 11 | # If a shell does not support `local`, it will be aliased to 12 | # `typeset`, so this check is not needed. 13 | has_local() { 14 | local _has_local 15 | } 16 | has_local 2>/dev/null || alias local=typeset 17 | 18 | set -o errexit # Exit on error 19 | set -o nounset # Treat unset variables as an error 20 | 21 | # Colours 22 | WHITE_BOLD='\033[1;37m' 23 | RESET='\033[0m' 24 | 25 | detect_osarch() { 26 | case $(uname -sm) in 27 | 'Linux x86_64') 28 | os='linux' 29 | arch='x86_64' 30 | ext='' 31 | ;; 32 | 'Linux aarch64') 33 | os='linux' 34 | arch='aarch64' 35 | ext='' 36 | ;; 37 | 'Darwin x86' | 'Darwin x86_64') 38 | os='osx' 39 | arch='x86_64' 40 | ext='' 41 | ;; 42 | 'Darwin arm64') 43 | os='osx' 44 | arch='aarch64' 45 | ext='' 46 | ;; 47 | CYGWIN* | MINGW32* | MSYS* | MINGW*) 48 | os="windows" 49 | arch='x86_64' 50 | ext='.exe' 51 | ;; 52 | *) 53 | echo "Sorry, you'll need to install the plugin CLI manually." 54 | exit 1 55 | ;; 56 | esac 57 | } 58 | 59 | install_pact_plugin_cli() { 60 | [ -f ~/.pact/bin/pact-plugin-cli ] && \ 61 | echo "${WHITE_BOLD}=> Plugin CLI already installed${RESET}" && \ 62 | return 63 | 64 | local version="0.1.2" 65 | detect_osarch 66 | local url="https://github.com/pact-foundation/pact-plugins/releases/download/pact-plugin-cli-v${version}/pact-plugin-cli-${os}-${arch}${ext}.gz" 67 | 68 | echo "${WHITE_BOLD}=> Installing plugins CLI version '${version}'${RESET}" 69 | echo " - OS: ${os}" 70 | echo " - Arch: ${arch}" 71 | echo " - Version: ${version}" 72 | echo " - URL: ${url}" 73 | echo " - Downloading into: ~/.pact/bin/" 74 | 75 | mkdir -p ~/.pact/bin 76 | 77 | if command -v curl >/dev/null 2>&1; then 78 | curl -sSL "$url" | gunzip -c > ~/.pact/bin/pact-plugin-cli 79 | elif command -v wget >/dev/null 2>&1; then 80 | wget -qO- "$url" | gunzip -c > ~/.pact/bin/pact-plugin-cli 81 | else 82 | echo "Neither curl nor wget found. Please install one of these packages." 83 | exit 1 84 | fi 85 | chmod +x ~/.pact/bin/pact-plugin-cli 86 | } 87 | 88 | install_matt_plugin() { 89 | [ -d ~/.pact/plugins/matt-0.1.1 ] && \ 90 | echo "${WHITE_BOLD}=> MATT plugin already installed${RESET}" && \ 91 | return 92 | 93 | local version="0.1.1" 94 | local url="https://github.com/mefellows/pact-matt-plugin/releases/tag/v${version}" 95 | 96 | echo "${WHITE_BOLD}=> Installing MATT plugin version '${version}'${RESET}" 97 | ~/.pact/bin/pact-plugin-cli install "$url" 98 | 99 | } 100 | 101 | main() { 102 | install_pact_plugin_cli 103 | install_matt_plugin 104 | } 105 | 106 | main 107 | -------------------------------------------------------------------------------- /scripts/run-audit-fix-on-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | set -eu 3 | 4 | ####### 5 | # Usage 6 | # run-audit-fix-on-examples.sh 7 | #### 8 | 9 | # Colours 10 | blue='\033[1;34m' 11 | red='\033[0;31m' 12 | green='\033[0;32m' 13 | end_colour='\033[0m' 14 | 15 | 16 | ADDITIONAL_ARG=${1:-} 17 | PROBLEM_REPOS=() 18 | 19 | banner_border() { 20 | banner_mid "$* " | sed 's/./-/g' 21 | } 22 | 23 | # banner based on https://unix.stackexchange.com/a/250094 24 | banner_mid() { 25 | echo "- $* " 26 | } 27 | 28 | fail_banner() { 29 | echo -e "${red}$(banner_border "❌ $*")${end_colour}" 30 | echo -e "${red}$(banner_mid "❌ $*")${end_colour}" 31 | echo -e "${red}$(banner_border "❌ $*")${end_colour}" 32 | } 33 | 34 | log_banner() { 35 | echo -e "${blue}$(banner_border "🛠️ $*")${end_colour}" 36 | echo -e "${blue}$(banner_mid "🛠️ $*")${end_colour}" 37 | echo -e "${blue}$(banner_border "🛠️ $*")${end_colour}" 38 | } 39 | 40 | success_banner() { 41 | echo -e "${green}$(banner_border "✅ $*")${end_colour}" 42 | echo -e "${green}$(banner_mid "✅ $*")${end_colour}" 43 | echo -e "${green}$(banner_border "✅ $*")${end_colour}" 44 | } 45 | 46 | audit_fix() { 47 | if [ -z "${1:-}" ]; then 48 | fail_banner "Script error: ${FUNCNAME[0]} requires an argument" 49 | exit 1 50 | fi 51 | 52 | if [ -d "$1" ] && [ -e "$i/package.json" ]; then 53 | log_banner "Audit fix on $1" 54 | if [ -z "${ADDITIONAL_ARG:-}" ]; then 55 | if ! ( cd "$1" ; npm i ; npm audit --audit-level none ; npm audit fix) 56 | then 57 | fail_banner "$i failed audit fix" 58 | PROBLEM_REPOS+=("$i") 59 | fi 60 | fi 61 | else 62 | log_banner "Skipping $1 as it is not a directory or does not have a package.json" 63 | fi 64 | } 65 | 66 | for i in examples/*; do 67 | audit_fix "$i" 68 | done 69 | 70 | for i in examples/v3/*; do 71 | audit_fix "$i" 72 | done 73 | 74 | for i in examples/v4/*; do 75 | audit_fix "$i" 76 | done 77 | 78 | if [ ${#PROBLEM_REPOS[@]} -eq 0 ]; then 79 | success_banner "All examples updated successfully" 80 | else 81 | fail_banner "The following examples failed to audit fix, and need manual attention:" 82 | 83 | echo -e "${red}$(printf ' %s\n' "${PROBLEM_REPOS[@]}")${end_colour}" 84 | exit 1 85 | fi -------------------------------------------------------------------------------- /scripts/trigger-9x-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to trigger release of the repository 4 | # Requires a Github API token with repo scope stored in the 5 | # environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES 6 | # Borrowed from Beth Skurrie's excellent script at 7 | # https://github.com/pact-foundation/pact-ruby/blob/master/script/trigger-release.sh 8 | 9 | : "${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES:?Please set environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}" 10 | 11 | repository_slug=$(git remote get-url $(git remote show) | cut -d':' -f2 | sed 's/\.git//' | sed 's://::' | sed 's:github.com/::') 12 | 13 | output=$(curl -v -X POST https://api.github.com/repos/${repository_slug}/dispatches \ 14 | -H 'Accept: application/vnd.github.everest-preview+json' \ 15 | -H "Authorization: Bearer $GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES" \ 16 | -d "{\"event_type\": \"release-9x-triggered\"}" 2>&1) 17 | 18 | if ! echo "${output}" | grep "HTTP\/2 204" > /dev/null; then 19 | echo "$output" | sed "s/${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}/********/g" 20 | echo "Failed to trigger release" 21 | exit 1 22 | else 23 | echo "9.x.x Release workflow triggered" 24 | fi 25 | -------------------------------------------------------------------------------- /scripts/trigger-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to trigger release of the repository 4 | # Requires a Github API token with repo scope stored in the 5 | # environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES 6 | # Borrowed from Beth Skurrie's excellent script at 7 | # https://github.com/pact-foundation/pact-ruby/blob/master/script/trigger-release.sh 8 | 9 | : "${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES:?Please set environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}" 10 | 11 | repository_slug=$(git remote get-url $(git remote show) | cut -d':' -f2 | sed 's/\.git//' | sed 's://::' | sed 's:github.com/::') 12 | 13 | output=$(curl -v -X POST https://api.github.com/repos/${repository_slug}/dispatches \ 14 | -H 'Accept: application/vnd.github.everest-preview+json' \ 15 | -H "Authorization: Bearer $GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES" \ 16 | -d "{\"event_type\": \"release-triggered\"}" 2>&1) 17 | 18 | if ! echo "${output}" | grep "HTTP\/2 204" > /dev/null; then 19 | echo "$output" | sed "s/${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}/********/g" 20 | echo "Failed to trigger release" 21 | exit 1 22 | else 23 | echo "11.x.x release workflow triggered" 24 | fi 25 | -------------------------------------------------------------------------------- /src/common/jsonTypes.ts: -------------------------------------------------------------------------------- 1 | export type AnyJson = boolean | number | string | null | JsonArray | JsonMap; 2 | export interface JsonMap { 3 | [key: string]: AnyJson; 4 | } 5 | export type JsonArray = Array; 6 | -------------------------------------------------------------------------------- /src/common/logger.ts: -------------------------------------------------------------------------------- 1 | import logger from '@pact-foundation/pact-core/src/logger'; 2 | 3 | import { version } from '../../package.json'; 4 | 5 | export * from '@pact-foundation/pact-core/src/logger'; 6 | 7 | const context = `pact@${version}`; 8 | 9 | export default { 10 | pactCrash: (message: string): void => logger.pactCrash(message, context), 11 | error: (message: string): void => logger.error(message, context), 12 | warn: (message: string): void => logger.warn(message, context), 13 | info: (message: string): void => logger.info(message, context), 14 | debug: (message: string): void => logger.debug(message, context), 15 | trace: (message: string): void => logger.trace(message, context), 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/net.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import nodeNet from 'net'; 4 | import { isPortAvailable } from './net'; 5 | import logger from './logger'; 6 | 7 | const { expect } = chai; 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | describe('Net', () => { 12 | const port = 4567; 13 | const defaultHost = '0.0.0.0'; 14 | const specialPort = -1; 15 | 16 | // Utility function to create a server on a given port and return a Promise 17 | const createServer = (p: number, host = defaultHost) => 18 | new Promise((resolve, reject) => { 19 | const server = nodeNet.createServer(); 20 | 21 | server.on('error', (err: any) => reject(err)); 22 | server.on('listening', () => resolve(server)); 23 | 24 | server.listen({ port: p, host, exclusive: true }, () => { 25 | logger.info(`test server is up on ${host}:${p}`); 26 | }); 27 | }); 28 | 29 | describe('#isPortAvailable', () => { 30 | context('when the port is not allowed to be bound', () => { 31 | it('returns a rejected promise', () => 32 | expect(isPortAvailable(specialPort, defaultHost)).to.eventually.be 33 | .rejected); 34 | }); 35 | 36 | context('when the port is available', () => { 37 | it('returns a fulfilled promise', () => 38 | expect(isPortAvailable(port, defaultHost)).to.eventually.be.fulfilled); 39 | }); 40 | 41 | context('when the port is unavailable', () => { 42 | let closeFn = (cb: any) => cb(); 43 | 44 | it('returns a rejected promise', () => 45 | createServer(port).then((server: { close(): any }) => { 46 | closeFn = server.close.bind(server); 47 | return expect(isPortAvailable(port, defaultHost)).to.eventually.be 48 | .rejected; 49 | })); 50 | 51 | // close the servers used in this test as to not conflict with other tests 52 | afterEach((done) => { 53 | closeFn(done); 54 | }); 55 | }); 56 | 57 | context('when a single host is unavailable', () => { 58 | let closeFn = (cb: any) => cb(); 59 | 60 | it('returns a fulfilled promise', () => 61 | // simulate ::1 being unavailable 62 | createServer(port, '::1').then((server: { close(): any }) => { 63 | closeFn = server.close.bind(server); 64 | // this should work as the `127.0.0.1` is NOT `::1` 65 | return expect(isPortAvailable(port, '127.0.0.1')).to.eventually.be 66 | .fulfilled; 67 | })); 68 | 69 | // close the servers used in this test as to not conflict with other tests 70 | afterEach((done) => { 71 | closeFn(done); 72 | }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/common/net.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Network module. 3 | * @module net 4 | * @private 5 | */ 6 | 7 | import net from 'net'; 8 | 9 | export const localAddresses = ['127.0.0.1', 'localhost', '0.0.0.0', '::1']; 10 | 11 | export const portCheck = (port: number, host: string): Promise => 12 | new Promise((resolve, reject) => { 13 | const server = net 14 | .createServer() 15 | .listen({ port, host, exclusive: true }) 16 | .on('error', (e: NodeJS.ErrnoException) => { 17 | if (e.code === 'EADDRINUSE') { 18 | reject(new Error(`Port ${port} is unavailable on address ${host}`)); 19 | } else { 20 | reject(e); 21 | } 22 | }) 23 | .on('listening', () => { 24 | server.once('close', () => resolve()).close(); 25 | }); 26 | }); 27 | 28 | export const isPortAvailable = (port: number, host: string): Promise => 29 | Promise.allSettled( 30 | localAddresses.map((localHost) => portCheck(port, localHost)) 31 | ).then((settledPortChecks) => { 32 | // if every port check failed, then fail the `isPortAvailable` check 33 | if (settledPortChecks.every((result) => result.status === 'rejected')) { 34 | throw new Error(`Cannot open port ${port} on ipv4 or ipv6 interfaces`); 35 | } 36 | 37 | // the local addresses passed - now check the host that the user has specified 38 | return portCheck(port, host); 39 | }); 40 | 41 | export const freePort = (): Promise => 42 | new Promise((res) => { 43 | const s = net.createServer(); 44 | s.listen(0, () => { 45 | const addr = s.address(); 46 | if (addr !== null && typeof addr !== 'string') { 47 | const { port } = addr; 48 | s.close(() => res(port)); 49 | } else { 50 | throw Error('unable to find a free port'); 51 | } 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/common/request.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import nock from 'nock'; 4 | import { HTTPMethods, Request } from './request'; 5 | 6 | chai.use(chaiAsPromised); 7 | 8 | const { expect } = chai; 9 | 10 | describe('Request', () => { 11 | let request: Request; 12 | const port = 1024 + Math.floor(Math.random() * 5000); 13 | const url = `http://localhost:${port}`; 14 | const urlSecure = `https://localhost:${port}`; 15 | 16 | beforeEach(() => { 17 | request = new Request(); 18 | }); 19 | 20 | context('#send', () => { 21 | afterEach(() => nock.cleanAll()); 22 | 23 | describe('Promise', () => { 24 | it('returns a promise', () => { 25 | nock(url).get('/').reply(200); 26 | const r = request.send(HTTPMethods.GET, url); 27 | return Promise.all([ 28 | expect(r).is.ok, 29 | expect(r.then).is.ok, 30 | expect(r.then).is.a('function'), 31 | expect(r).to.be.fulfilled, 32 | ]); 33 | }); 34 | 35 | it('resolves when request succeeds with response body', () => { 36 | const body = 'body'; 37 | nock(url).get('/').reply(200, body); 38 | const p = request.send(HTTPMethods.GET, url); 39 | return Promise.all([ 40 | expect(p).to.be.fulfilled, 41 | expect(p).to.eventually.be.equal(body), 42 | ]); 43 | }); 44 | 45 | it('rejects when request fails with error message', () => { 46 | const error = 'error'; 47 | nock(url).get('/').reply(400, error); 48 | const p = request.send(HTTPMethods.GET, url); 49 | return expect(p).to.be.rejectedWith(error); 50 | }); 51 | }); 52 | describe('Headers', () => { 53 | it('sends Pact headers are sent with every request', () => { 54 | nock(url) 55 | .matchHeader('X-Pact-Mock-Service', 'true') 56 | .get('/') 57 | .reply(200); 58 | return expect(request.send(HTTPMethods.GET, url)).to.be.fulfilled; 59 | }); 60 | }); 61 | describe('SSL', () => { 62 | it('ignores self signed certificate errors', () => { 63 | nock(urlSecure) 64 | .matchHeader('X-Pact-Mock-Service', 'true') 65 | .get('/') 66 | .reply(200); 67 | return expect(request.send(HTTPMethods.GET, urlSecure)).to.be.fulfilled; 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/common/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import https from 'https'; 3 | import { pathOr } from 'ramda'; 4 | import logger from './logger'; 5 | 6 | export enum HTTPMethods { 7 | GET = 'GET', 8 | POST = 'POST', 9 | PUT = 'PUT', 10 | PATCH = 'PATCH', 11 | DELETE = 'DELETE', 12 | HEAD = 'HEAD', 13 | OPTIONS = 'OPTIONS', 14 | COPY = 'COPY', 15 | LOCK = 'LOCK', 16 | MKCOL = 'MKCOL', 17 | MOVE = 'MOVE', 18 | PROPFIND = 'PROPFIND', 19 | PROPPATCH = 'PROPPATCH', 20 | UNLOCK = 'UNLOCK', 21 | REPORT = 'REPORT', 22 | } 23 | 24 | export type HTTPMethod = 25 | | 'GET' 26 | | 'POST' 27 | | 'PUT' 28 | | 'PATCH' 29 | | 'DELETE' 30 | | 'HEAD' 31 | | 'OPTIONS' 32 | | 'COPY' 33 | | 'LOCK' 34 | | 'MKCOL' 35 | | 'MOVE' 36 | | 'PROPFIND' 37 | | 'PROPPATCH' 38 | | 'UNLOCK' 39 | | 'REPORT'; 40 | 41 | export class Request { 42 | public async send( 43 | method: HTTPMethod, 44 | url: string, 45 | body?: string 46 | ): Promise { 47 | try { 48 | const res = await axios(url, { 49 | data: body, 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | 'X-Pact-Mock-Service': 'true', 53 | }, 54 | httpsAgent: new https.Agent({ 55 | keepAlive: true, 56 | rejectUnauthorized: false, 57 | }), 58 | method, 59 | timeout: 10000, 60 | url, 61 | maxBodyLength: Infinity, 62 | }); 63 | 64 | if (res.status >= 200 && res.status < 400) { 65 | return res.data; 66 | } 67 | return await Promise.reject(res.data); 68 | } catch (e) { 69 | logger.error(`error making http request: ${e.message}`); 70 | return Promise.reject(pathOr(e.message, ['response', 'data'], e)); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/common/spec.ts: -------------------------------------------------------------------------------- 1 | import { SpecificationVersion } from '../v3'; 2 | 3 | export const numberToSpec = ( 4 | spec?: number, 5 | defaultSpec: SpecificationVersion = SpecificationVersion.SPECIFICATION_VERSION_V2 6 | ): SpecificationVersion => { 7 | if (!spec) { 8 | return defaultSpec; 9 | } 10 | 11 | switch (spec) { 12 | case 2: 13 | return SpecificationVersion.SPECIFICATION_VERSION_V2; 14 | case 3: 15 | return SpecificationVersion.SPECIFICATION_VERSION_V3; 16 | case 4: 17 | return SpecificationVersion.SPECIFICATION_VERSION_V4; 18 | default: 19 | throw new Error(`invalid pact specification version supplied: ${spec}`); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/dsl/apolloGraphql.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { ApolloGraphQLInteraction } from './apolloGraphql'; 4 | 5 | chai.use(chaiAsPromised); 6 | const { expect } = chai; 7 | 8 | describe('ApolloGraphQLInteraction', () => { 9 | let interaction: ApolloGraphQLInteraction; 10 | 11 | beforeEach(() => { 12 | interaction = new ApolloGraphQLInteraction(); 13 | }); 14 | 15 | describe('#withVariables', () => { 16 | describe('when given a set of variables', () => { 17 | it('adds the variables to the payload', () => { 18 | interaction.uponReceiving('a request'); 19 | interaction.withRequest({ 20 | path: '/graphql', 21 | method: 'POST', 22 | }); 23 | interaction.withOperation('query'); 24 | interaction.withQuery('{ hello }'); 25 | interaction.withVariables({ 26 | foo: 'bar', 27 | }); 28 | interaction.willRespondWith({ 29 | status: 200, 30 | body: { data: {} }, 31 | }); 32 | 33 | const json: any = interaction.json(); 34 | expect(json.request.body.variables).to.deep.eq({ foo: 'bar' }); 35 | }); 36 | }); 37 | 38 | describe('when no variables are presented', () => { 39 | it('adds an empty variables property to the payload', () => { 40 | interaction.uponReceiving('a request'); 41 | interaction.withRequest({ 42 | path: '/graphql', 43 | method: 'POST', 44 | }); 45 | interaction.withOperation('query'); 46 | interaction.withQuery('{ hello }'); 47 | interaction.willRespondWith({ 48 | status: 200, 49 | body: { data: {} }, 50 | }); 51 | 52 | const json: any = interaction.json(); 53 | expect(json.request.body).to.have.property('variables'); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('#withOperation', () => { 59 | describe('when no operationName is presented', () => { 60 | it('adds a null operationName property to the payload', () => { 61 | interaction.uponReceiving('a request'); 62 | interaction.withRequest({ 63 | path: '/graphql', 64 | method: 'POST', 65 | }); 66 | interaction.withQuery('{ hello }'); 67 | interaction.willRespondWith({ 68 | status: 200, 69 | body: { data: {} }, 70 | }); 71 | 72 | const json: any = interaction.json(); 73 | expect(json.request.body).to.have.property('operationName'); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/dsl/apolloGraphql.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInteraction } from './graphql'; 2 | 3 | export class ApolloGraphQLInteraction extends GraphQLInteraction { 4 | constructor() { 5 | super(); 6 | this.variables = this.variables || {}; 7 | this.operation = this.operation || null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/dsl/message.ts: -------------------------------------------------------------------------------- 1 | import { AnyJson } from '../common/jsonTypes'; 2 | import { Matcher } from './matchers'; 3 | import { Matcher as MatcherV3 } from '../v3/matchers'; 4 | 5 | /** 6 | * Metadata is a map containing message context, 7 | * such as content-type, correlation IDs etc. 8 | * 9 | * @module Message 10 | */ 11 | export interface Metadata { 12 | [name: string]: string | Matcher | MatcherV3; 13 | } 14 | 15 | /** 16 | * Defines a state a provider must be in. 17 | */ 18 | export interface ProviderState { 19 | name: string; 20 | params?: { 21 | [name: string]: string; 22 | }; 23 | } 24 | 25 | /** 26 | * A Message is an asynchronous Interaction, sent via a Provider 27 | * 28 | * @module Message 29 | */ 30 | export interface Message { 31 | providerStates?: ProviderState[]; 32 | description?: string; 33 | metadata?: Metadata; 34 | contents: unknown | Buffer; 35 | } 36 | 37 | export interface ConcreteMessage { 38 | providerStates?: ProviderState[]; 39 | description?: string; 40 | metadata?: Metadata; 41 | contents: AnyJson | Buffer; 42 | } 43 | 44 | /** 45 | * A Message Descriptor is a set of additional context for a given message 46 | * 47 | * @module Message 48 | */ 49 | export interface MessageDescriptor { 50 | providerStates?: ProviderState[]; 51 | description: string; 52 | metadata?: Metadata; 53 | } 54 | 55 | /** 56 | * A Message Consumer is a function that will receive a message 57 | * from a given Message Provider. It is given the full Message 58 | * context during verification. 59 | * 60 | * @module Message 61 | */ 62 | export type MessageConsumer = (m: ConcreteMessage) => Promise; 63 | 64 | export type MessageFromProvider = unknown; 65 | export type MessageFromProviderWithMetadata = { 66 | __pactMessageMetadata: Record; 67 | message: unknown; 68 | }; 69 | 70 | /** 71 | * A Message Provider is a function that will be invoked by the framework 72 | * in order to _produce_ a message for a consumer. The response must match what 73 | * the given consumer has specified in the pact file. It is given a Message 74 | * Descriptor object when being invoked which can be used for additional context. 75 | * 76 | * @module Message 77 | */ 78 | export type MessageProvider = ( 79 | m: MessageDescriptor 80 | ) => 81 | | Promise 82 | | MessageFromProvider 83 | | MessageFromProviderWithMetadata; 84 | 85 | export interface MessageProviders { 86 | [name: string]: MessageProvider; 87 | } 88 | 89 | export interface MessageStateHandlers { 90 | [name: string]: ( 91 | state: string, 92 | params?: { [name: string]: string } 93 | ) => Promise; 94 | } 95 | -------------------------------------------------------------------------------- /src/dsl/mockService.ts: -------------------------------------------------------------------------------- 1 | // Control how the Pact files are written 2 | // - overwrite: Recreates the target pact file after each run of Pact, 3 | // clobbering any existing pacts if present. 4 | // - merge: Tells Pact that it will merge in new interactions to an 5 | // existing file, to allow multiple test files to be run in parallel. 6 | // This mode is really only useful if you need to split tests for a 7 | // single consumer-provider pair, across multiple test files. 8 | // You should delete any existing pacts before running the tests 9 | // so that interactions deleted from the code are not maintained in the file 10 | // - update: Appends or updates interactions in a pact file. If an interaction 11 | // exists in the file, it is updated. 12 | export type PactfileWriteMode = 'overwrite' | 'update' | 'merge'; 13 | 14 | export interface Pacticipant { 15 | name: string; 16 | } 17 | 18 | export interface PactDetails { 19 | consumer?: Pacticipant; 20 | provider?: Pacticipant; 21 | pactfile_write_mode: PactfileWriteMode; 22 | } 23 | 24 | export interface MockService { 25 | pactDetails: PactDetails; 26 | baseUrl: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/dsl/options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pact Options module. 3 | * @module PactOptions 4 | */ 5 | import { VerifierOptions as PactCoreVerifierOptions } from '@pact-foundation/pact-core'; 6 | import { PactfileWriteMode } from './mockService'; 7 | import { MessageProviders, MessageStateHandlers } from './message'; 8 | 9 | export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error'; 10 | 11 | export interface PactOptions { 12 | // The name of the consumer 13 | consumer: string; 14 | 15 | // The name of the provider 16 | provider: string; 17 | 18 | // The port to run the mock service on, defaults to 1234 19 | port?: number; 20 | 21 | // The host to run the mock service, defaults to 127.0.0.1 22 | host?: string; 23 | 24 | // SSL flag to identify the protocol to be used (default false, HTTP) 25 | ssl?: boolean; 26 | 27 | // Path to SSL certificate to serve on the mock service 28 | sslcert?: string; 29 | 30 | // Path to SSL key to serve on the mock service 31 | sslkey?: string; 32 | 33 | // Directory to output pact files 34 | dir?: string; 35 | 36 | // Directory to log to 37 | log?: string; 38 | 39 | // Log level 40 | logLevel?: LogLevel; 41 | 42 | // Pact specification version (defaults to 2) 43 | spec?: number; 44 | 45 | // Allow CORS OPTION requests to be accepted, defaults to false 46 | cors?: boolean; 47 | 48 | // How long to wait for the server to start before timing out 49 | timeout?: number; 50 | 51 | // Control how the Pact files are written 52 | // (defaults to 'overwrite') 53 | pactfileWriteMode?: PactfileWriteMode; 54 | } 55 | 56 | export interface MandatoryPactOptions { 57 | port: number; 58 | host: string; 59 | ssl: boolean; 60 | } 61 | 62 | export type PactOptionsComplete = PactOptions & MandatoryPactOptions; 63 | 64 | export interface MessageProviderOptions { 65 | // Log level 66 | logLevel?: LogLevel; 67 | 68 | // Message providers 69 | messageProviders: MessageProviders; 70 | 71 | // Prepare any provider states 72 | stateHandlers?: MessageStateHandlers; 73 | } 74 | 75 | type ExcludedPactNodeVerifierKeys = Exclude< 76 | keyof PactCoreVerifierOptions, 77 | 'providerBaseUrl' 78 | >; 79 | export type PactNodeVerificationExcludedOptions = Pick< 80 | PactCoreVerifierOptions, 81 | ExcludedPactNodeVerifierKeys 82 | >; 83 | 84 | export type PactMessageProviderOptions = PactNodeVerificationExcludedOptions & 85 | MessageProviderOptions; 86 | 87 | export interface MessageConsumerOptions { 88 | // The name of the consumer 89 | consumer: string; 90 | 91 | // Directory to output pact files 92 | dir?: string; 93 | 94 | // The name of the provider 95 | provider: string; 96 | 97 | // Directory to log to 98 | log?: string; 99 | 100 | // Log level 101 | logLevel?: LogLevel; 102 | 103 | // Specification Version (should be 3 or greater for messages) 104 | spec?: number; 105 | 106 | // Control how the Pact files are written 107 | // Choices: 'overwrite' | 'update', 'none', defaults to 'overwrite' 108 | pactfileWriteMode?: PactfileWriteMode; 109 | } 110 | -------------------------------------------------------------------------------- /src/dsl/verifier/index.ts: -------------------------------------------------------------------------------- 1 | export * from './verifier'; 2 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/hooks.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /** 3 | * These handlers assume that the number of "setup" and "teardown" requests to 4 | * `/_pactSetup` are always sequential and balanced, i.e. if 3 "setup" actions 5 | * are received prior to an interaction being executed, then 3 "teardown" 6 | * actions will be received after that interaction has ended. 7 | */ 8 | import { RequestHandler } from 'express'; 9 | 10 | import logger from '../../../common/logger'; 11 | import { Hook } from './types'; 12 | 13 | export type HooksState = { 14 | setupCounter: number; 15 | }; 16 | 17 | export const registerHookStateTracking = 18 | (hooksState: HooksState): RequestHandler => 19 | async ({ body }, res, next) => { 20 | if (body?.action === 'setup') hooksState.setupCounter += 1; 21 | if (body?.action === 'teardown') hooksState.setupCounter -= 1; 22 | 23 | logger.debug( 24 | `hooks state counter is ${hooksState.setupCounter} after receiving "${body?.action}" action` 25 | ); 26 | 27 | next(); 28 | }; 29 | 30 | export const registerBeforeHook = 31 | (beforeEach: Hook, hooksState: HooksState): RequestHandler => 32 | async ({ body }, res, next) => { 33 | if (body?.action === 'setup' && hooksState.setupCounter === 1) { 34 | logger.debug("executing 'beforeEach' hook"); 35 | try { 36 | await beforeEach(); 37 | next(); 38 | } catch (e) { 39 | logger.error(`error executing 'beforeEach' hook: ${e.message}`); 40 | logger.debug(`Stack trace was: ${e.stack}`); 41 | next(new Error(`error executing 'beforeEach' hook: ${e.message}`)); 42 | } 43 | } else { 44 | next(); 45 | } 46 | }; 47 | 48 | export const registerAfterHook = 49 | (afterEach: Hook, hooksState: HooksState): RequestHandler => 50 | async ({ body }, res, next) => { 51 | if (body?.action === 'teardown' && hooksState.setupCounter === 0) { 52 | logger.debug("executing 'afterEach' hook"); 53 | try { 54 | await afterEach(); 55 | next(); 56 | } catch (e) { 57 | logger.error(`error executing 'afterEach' hook: ${e.message}`); 58 | logger.debug(`Stack trace was: ${e.stack}`); 59 | next(new Error(`error executing 'afterEach' hook: ${e.message}`)); 60 | } 61 | } else { 62 | next(); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy'; 2 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/parseBody.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import { parseBody } from './parseBody'; 5 | 6 | chai.use(chaiAsPromised); 7 | 8 | const { expect } = chai; 9 | 10 | describe('Verifier', () => { 11 | describe('#parseBody', () => { 12 | describe('when request body exists', () => { 13 | it('it returns the request body buffer', async () => { 14 | const req: any = { body: '' }; 15 | req.body = Buffer.from('foo'); 16 | 17 | const body = parseBody(req); 18 | 19 | expect(body).to.be.instanceOf(Buffer); 20 | expect(body.toString()).to.eq('foo'); 21 | }); 22 | 23 | it('it returns a buffer of the request body object', async () => { 24 | const req: any = { body: { foo: 'bar' } }; 25 | 26 | const body = parseBody(req); 27 | 28 | expect(body).to.be.instanceOf(Buffer); 29 | expect(body.toString()).to.eq(JSON.stringify(req.body)); 30 | }); 31 | }); 32 | 33 | describe('when request body does not exist', () => { 34 | it('returns an empty buffer', async () => { 35 | const req: any = 'foo'; 36 | 37 | const body = parseBody(req); 38 | 39 | expect(body).to.be.instanceOf(Buffer); 40 | expect(body).to.not.have.length; 41 | expect(body.toString()).to.be.empty; 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/parseBody.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | 3 | interface ReqBodyExtended extends http.IncomingMessage { 4 | body?: Buffer | Record; 5 | } 6 | 7 | export const parseBody = (req: ReqBodyExtended): Buffer => { 8 | let bodyData = Buffer.alloc(0); 9 | 10 | if (!req.body || !Object.keys(req.body).length) { 11 | return bodyData; 12 | } 13 | 14 | if (Buffer.isBuffer(req.body)) { 15 | bodyData = Buffer.from(req.body); 16 | } else if (typeof req.body === 'object') { 17 | bodyData = Buffer.from(JSON.stringify(req.body)); 18 | } 19 | 20 | return bodyData; 21 | }; 22 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/proxy.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import http from 'http'; 5 | 6 | import { waitForServerReady } from './proxy'; 7 | 8 | chai.use(chaiAsPromised); 9 | 10 | const { expect } = chai; 11 | 12 | // Little function to mock out an Event Emitter 13 | const fakeServer = (event: string) => ({ 14 | on: (registeredEvent: string, cb: any) => { 15 | if (registeredEvent === event) { 16 | cb(); 17 | } 18 | }, 19 | }); 20 | 21 | describe('#waitForServerReady', () => { 22 | context('when the server starts successfully', () => { 23 | it('returns a successful promise', () => { 24 | const res = waitForServerReady(fakeServer('listening') as http.Server); 25 | 26 | return expect(res).to.eventually.be.fulfilled; 27 | }); 28 | }); 29 | 30 | context('when the server fails to start', () => { 31 | it('returns an error', () => { 32 | const res = waitForServerReady(fakeServer('error') as http.Server); 33 | 34 | return expect(res).to.eventually.be.rejected; 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/proxyRequest.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import { Readable } from 'stream'; 3 | import { ProxyOptions } from './types'; 4 | import { toServerOptions as toServerOptionsAct } from './proxyRequest'; 5 | 6 | const { expect } = chai; 7 | 8 | describe('#toServerOptions', () => { 9 | const toServerOptions = (opts: ProxyOptions = {}, req?: { body: any }) => 10 | toServerOptionsAct(opts, req ?? ({} as any)); 11 | 12 | context('changeOrigin', () => { 13 | it('forwards option', () => { 14 | const res = toServerOptions({ changeOrigin: true }); 15 | 16 | expect(res.changeOrigin).to.be.true; 17 | }); 18 | 19 | it('is false by default', () => { 20 | const res = toServerOptions(); 21 | 22 | expect(res.changeOrigin).to.be.false; 23 | }); 24 | }); 25 | 26 | context('secure', () => { 27 | it('is true when validating ssl', () => { 28 | const res = toServerOptions({ validateSSL: true }); 29 | 30 | expect(res.secure).to.be.true; 31 | }); 32 | 33 | it('is false by default', () => { 34 | const res = toServerOptions(); 35 | 36 | expect(res.secure).to.be.false; 37 | }); 38 | }); 39 | 40 | context('target', () => { 41 | it('uses providerBaseUrl', () => { 42 | const expectedTarget = 'http://test.com'; 43 | 44 | const res = toServerOptions({ providerBaseUrl: expectedTarget }); 45 | 46 | expect(res.target).to.eq(expectedTarget); 47 | }); 48 | 49 | it('uses loopback address by default', () => { 50 | const res = toServerOptions(); 51 | 52 | expect(res.target).to.eq('http://127.0.0.1/'); 53 | }); 54 | }); 55 | 56 | context('agent', () => { 57 | const initialEnv = { ...process.env }; 58 | 59 | afterEach(() => { 60 | process.env = { ...initialEnv }; 61 | }); 62 | 63 | it('uses no agent by default', () => { 64 | const res = toServerOptions(); 65 | 66 | expect(res.agent).to.be.undefined; 67 | }); 68 | 69 | it('uses HTTPS_PROXY', () => { 70 | const expectedProxy = 'http://proxy.host/'; 71 | process.env.HTTPS_PROXY = expectedProxy; 72 | 73 | const res = toServerOptions(); 74 | 75 | expect(res.agent?.proxy?.toString()).to.eq(expectedProxy); 76 | }); 77 | 78 | it('uses HTTP_PROXY', () => { 79 | const expectedProxy = 'http://my.proxy/'; 80 | process.env.HTTP_PROXY = expectedProxy; 81 | 82 | const res = toServerOptions(); 83 | 84 | expect(res.agent?.proxy?.toString()).to.eq(expectedProxy); 85 | }); 86 | 87 | it('prefers HTTPS_PROXY to HTTP_PROXY', () => { 88 | process.env.HTTPS_PROXY = 'http://unused/'; 89 | const expectedProxy = 'http://expected.proxy/'; 90 | process.env.HTTPS_PROXY = expectedProxy; 91 | 92 | const res = toServerOptions(); 93 | 94 | expect(res.agent?.proxy?.toString()).to.eq(expectedProxy); 95 | }); 96 | }); 97 | 98 | context('buffer', () => { 99 | it('provides readable of body', () => { 100 | const res = toServerOptions({}, { body: 'a' }); 101 | 102 | expect(res.buffer).to.be.instanceOf(Readable); 103 | }); 104 | 105 | it('provides readable when body is undefined', () => { 106 | const res = toServerOptions(); 107 | 108 | expect(res.buffer).to.be.instanceOf(Readable); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/proxyRequest.ts: -------------------------------------------------------------------------------- 1 | import { HttpsProxyAgent } from 'https-proxy-agent'; 2 | import { ServerOptions } from 'http-proxy'; 3 | import { Readable } from 'stream'; 4 | import { IncomingMessage } from 'http'; 5 | import { ProxyOptions } from './types'; 6 | import { parseBody } from './parseBody'; 7 | 8 | // A base URL is always needed for the proxy, even 9 | // if there are no targets to proxy (e.g. in the case 10 | // of message pact 11 | const defaultBaseURL = () => 'http://127.0.0.1/'; 12 | 13 | export const toServerOptions = ( 14 | config: ProxyOptions, 15 | req: IncomingMessage 16 | ): ServerOptions => { 17 | // Provide direct support for standard proxy configuration 18 | const systemProxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; 19 | 20 | return { 21 | changeOrigin: config.changeOrigin === true, 22 | secure: config.validateSSL === true, 23 | target: config.providerBaseUrl || defaultBaseURL(), 24 | agent: systemProxy && new HttpsProxyAgent(systemProxy), 25 | buffer: Readable.from(parseBody(req)), 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/stateHandler/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stateHandler'; 2 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/stateHandler/setupStates.ts: -------------------------------------------------------------------------------- 1 | import logger from '../../../../common/logger'; 2 | import { 3 | ProxyOptions, 4 | StateFunc, 5 | StateFuncWithSetup, 6 | ProviderState, 7 | StateHandler, 8 | } from '../types'; 9 | import { JsonMap } from '../../../../common/jsonTypes'; 10 | 11 | const isStateFuncWithSetup = ( 12 | fn: StateFuncWithSetup | StateFunc 13 | ): fn is StateFuncWithSetup => 14 | (fn as StateFuncWithSetup).setup !== undefined || 15 | (fn as StateFuncWithSetup).teardown !== undefined; 16 | 17 | // Transform a regular state function to one with the setup/teardown functions 18 | const transformStateFunc = (fn: StateHandler): StateFuncWithSetup => 19 | isStateFuncWithSetup(fn) ? fn : { setup: fn }; 20 | 21 | // Lookup the handler based on the description 22 | export const setupStates = ( 23 | state: ProviderState, 24 | config: ProxyOptions 25 | ): Promise => { 26 | logger.debug(`setting up state '${JSON.stringify(state)}'`); 27 | 28 | const handler = config.stateHandlers 29 | ? config.stateHandlers[state.state] 30 | : null; 31 | 32 | if (!handler) { 33 | if (state.action === 'setup') { 34 | logger.warn(`no state handler found for state: "${state.state}"`); 35 | } 36 | return Promise.resolve(); 37 | } 38 | 39 | const stateFn = transformStateFunc(handler); 40 | switch (state.action) { 41 | case 'setup': 42 | if (stateFn.setup) { 43 | logger.debug(`setting up state '${state.state}'`); 44 | return stateFn.setup(state.params); 45 | } 46 | break; 47 | case 'teardown': 48 | if (stateFn.teardown) { 49 | logger.debug(`tearing down state '${state.state}'`); 50 | return stateFn.teardown(state.params); 51 | } 52 | break; 53 | default: 54 | logger.debug(`unknown state action '${state.action}' received, ignoring`); 55 | } 56 | 57 | return Promise.resolve(); 58 | }; 59 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/stateHandler/stateHandler.spec.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | 4 | import express from 'express'; 5 | 6 | import { createProxyStateHandler } from './stateHandler'; 7 | import { ProxyOptions, StateHandlers } from '../types'; 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | const { expect } = chai; 12 | 13 | describe('#createProxyStateHandler', () => { 14 | const state = { 15 | state: 'thing exists', 16 | action: 'setup', 17 | }; 18 | 19 | let res: any; 20 | const mockResponse = { 21 | status: (status: number) => { 22 | res = status; 23 | return { 24 | send: () => {}, 25 | }; 26 | }, 27 | json: (data: any) => data, 28 | }; 29 | 30 | context('when valid state handlers are provided', () => { 31 | it('returns a 200', async () => { 32 | const stateHandlers = { 33 | 'thing exists': () => Promise.resolve(), 34 | }; 35 | 36 | const h = createProxyStateHandler({ 37 | stateHandlers, 38 | } as ProxyOptions); 39 | return expect( 40 | h( 41 | { 42 | body: state, 43 | } as express.Request, 44 | mockResponse as express.Response 45 | ) 46 | ).to.eventually.be.fulfilled; 47 | }); 48 | }); 49 | 50 | context('when there is a problem with a state handler', () => { 51 | const badStateHandlers: StateHandlers = { 52 | 'thing exists': { 53 | setup: () => Promise.reject(new Error('bad')), 54 | }, 55 | }; 56 | 57 | it('returns a 500', async () => { 58 | const h = createProxyStateHandler({ 59 | stateHandlers: badStateHandlers, 60 | } as ProxyOptions); 61 | await h( 62 | { 63 | body: state, 64 | } as express.Request, 65 | mockResponse as express.Response 66 | ); 67 | 68 | expect(res).to.eql(500); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/stateHandler/stateHandler.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { ProxyOptions, ProviderState } from '../types'; 4 | import { setupStates } from './setupStates'; 5 | 6 | export const createProxyStateHandler = 7 | (config: ProxyOptions) => 8 | (req: express.Request, res: express.Response): Promise => { 9 | const message: ProviderState = req.body; 10 | 11 | return Promise.resolve(setupStates(message, config)) 12 | .then((data) => res.json(data)) 13 | .catch((e) => res.status(500).send(e)); 14 | }; 15 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/tracer.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { pickBy, identity, reduce, Dictionary } from 'lodash'; 3 | 4 | import logger from '../../../common/logger'; 5 | 6 | const removeEmptyResponseProperties = (body: string, res: express.Response) => 7 | pickBy( 8 | { 9 | body, 10 | headers: reduce( 11 | res.getHeaders(), 12 | ( 13 | acc: Dictionary, 14 | val, 15 | index 16 | ) => { 17 | acc[index] = val; 18 | return acc; 19 | }, 20 | {} 21 | ), 22 | status: res.statusCode, 23 | }, 24 | identity 25 | ); 26 | 27 | const removeEmptyRequestProperties = (req: express.Request) => 28 | pickBy( 29 | { 30 | body: req.body, 31 | headers: req.headers, 32 | method: req.method, 33 | path: req.path, 34 | }, 35 | identity 36 | ); 37 | 38 | export const createResponseTracer = 39 | (): express.RequestHandler => (_, res, next) => { 40 | const [oldWrite, oldEnd] = [res.write, res.end]; 41 | const chunks: Buffer[] = []; 42 | 43 | res.write = (chunk: Parameters[0]) => { 44 | chunks.push(Buffer.from(chunk)); 45 | return oldWrite.apply(res, [chunk]); 46 | }; 47 | 48 | res.end = (chunk: Parameters[0]) => { 49 | if (chunk) { 50 | chunks.push(Buffer.from(chunk)); 51 | } 52 | const body = Buffer.concat(chunks).toString('utf8'); 53 | logger.debug( 54 | `outgoing response: ${JSON.stringify( 55 | removeEmptyResponseProperties(body, res) 56 | )}` 57 | ); 58 | return oldEnd.apply(res, [chunk]); 59 | }; 60 | if (typeof next === 'function') { 61 | next(); 62 | } 63 | }; 64 | 65 | export const createRequestTracer = 66 | (): express.RequestHandler => (req, _, next) => { 67 | logger.debug( 68 | `incoming request: ${JSON.stringify(removeEmptyRequestProperties(req))}` 69 | ); 70 | next(); 71 | }; 72 | -------------------------------------------------------------------------------- /src/dsl/verifier/proxy/types.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { LogLevel } from '../../options'; 3 | import { JsonMap, AnyJson } from '../../../common/jsonTypes'; 4 | import { MessageProviders } from '../../message'; 5 | 6 | export type Hook = () => Promise; 7 | 8 | /** 9 | * State handlers map a state description to a function 10 | * that can setup the provider state 11 | */ 12 | export interface StateHandlers { 13 | [name: string]: StateHandler; 14 | } 15 | /** 16 | * Incoming provider state request 17 | */ 18 | export interface ProviderState { 19 | action: StateAction; 20 | params: JsonMap; 21 | state: string; 22 | } 23 | 24 | /** 25 | * Specifies whether the state handler being setup or shutdown 26 | */ 27 | export type StateAction = 'setup' | 'teardown'; 28 | 29 | /** 30 | * Respond to the state setup event, optionally returning a map of provider 31 | * values to dynamically inject into the incoming request to test 32 | */ 33 | export type StateFunc = (parameters?: AnyJson) => Promise; 34 | 35 | /** 36 | * Respond to the state setup event, with the ability to hook into the setup/teardown 37 | * phase of the state 38 | */ 39 | export type StateFuncWithSetup = { 40 | setup?: StateFunc; 41 | teardown?: StateFunc; 42 | }; 43 | 44 | export type StateHandler = StateFuncWithSetup | StateFunc; 45 | 46 | export interface ProxyOptions { 47 | logLevel?: LogLevel; 48 | requestFilter?: express.RequestHandler; 49 | stateHandlers?: StateHandlers; 50 | messageProviders?: MessageProviders; 51 | beforeEach?: Hook; 52 | afterEach?: Hook; 53 | validateSSL?: boolean; 54 | changeOrigin?: boolean; 55 | providerBaseUrl?: string; 56 | proxyHost?: string; 57 | } 58 | -------------------------------------------------------------------------------- /src/dsl/verifier/types.ts: -------------------------------------------------------------------------------- 1 | import { VerifierOptions as PactCoreVerifierOptions } from '@pact-foundation/pact-core'; 2 | import { MessageProviderOptions } from '../options'; 3 | 4 | import { ProxyOptions } from './proxy/types'; 5 | 6 | type ExcludedPactNodeVerifierKeys = Exclude< 7 | keyof PactCoreVerifierOptions, 8 | 'providerBaseUrl' 9 | >; 10 | 11 | export type PactNodeVerificationExcludedOptions = Pick< 12 | PactCoreVerifierOptions, 13 | ExcludedPactNodeVerifierKeys 14 | >; 15 | 16 | export type VerifierOptions = PactNodeVerificationExcludedOptions & 17 | ProxyOptions & 18 | Partial; 19 | -------------------------------------------------------------------------------- /src/errors/configurationError.ts: -------------------------------------------------------------------------------- 1 | export default class ConfigurationError extends Error {} 2 | -------------------------------------------------------------------------------- /src/errors/graphQLQueryError.ts: -------------------------------------------------------------------------------- 1 | export default class GraphQLQueryError extends Error {} 2 | -------------------------------------------------------------------------------- /src/errors/matcherError.ts: -------------------------------------------------------------------------------- 1 | export default class MatcherError extends Error {} 2 | -------------------------------------------------------------------------------- /src/errors/verificationError.ts: -------------------------------------------------------------------------------- 1 | export default class VerificationError extends Error {} 2 | -------------------------------------------------------------------------------- /src/httpPact/ffi.spec.ts: -------------------------------------------------------------------------------- 1 | import { ConsumerInteraction } from '@pact-foundation/pact-core'; 2 | import * as chai from 'chai'; 3 | import chaiAsPromised from 'chai-as-promised'; 4 | import sinon from 'sinon'; 5 | import sinonChai from 'sinon-chai'; 6 | import { contentTypeFromHeaders, setQuery } from './ffi'; 7 | 8 | chai.use(sinonChai); 9 | chai.use(chaiAsPromised); 10 | 11 | const { expect } = chai; 12 | 13 | describe('Pact FFI', () => { 14 | describe('#contentTypeFromHeaders', () => { 15 | ['content-type', 'Content-Type', 'CONTent-TYPE'].forEach((t) => { 16 | describe(`when the "${t}" header is set`, () => { 17 | it('detects the content type from the header', () => { 18 | const headers = { [t]: 'some-mime-type' }; 19 | expect(contentTypeFromHeaders(headers, 'application/json')).to.eq( 20 | 'some-mime-type' 21 | ); 22 | }); 23 | }); 24 | }); 25 | 26 | describe(`when the no content-type header is set`, () => { 27 | it('uses a default', () => { 28 | expect(contentTypeFromHeaders({}, 'application/json')).to.eq( 29 | 'application/json' 30 | ); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('#setQuery', () => { 36 | describe('with array values', () => { 37 | it('calls the query ffi function for each value', () => { 38 | const queryMock = sinon.stub(); 39 | 40 | const interaction = { 41 | withQuery: queryMock, 42 | } as unknown as ConsumerInteraction; // TODO replace with proper mock 43 | const query = { 44 | foo: ['bar', 'baz'], 45 | }; 46 | setQuery(interaction, query); 47 | expect(queryMock.calledTwice); 48 | expect(queryMock.calledWith('foo', 0, 'bar')); 49 | expect(queryMock.calledWith('foo', 1, 'baz')); 50 | }); 51 | }); 52 | 53 | describe('with single values', () => { 54 | it('calls the query ffi function for each value', () => { 55 | const queryMock = sinon.stub(); 56 | 57 | const interaction = { 58 | withQuery: queryMock, 59 | } as unknown as ConsumerInteraction; // TODO replace with proper mock 60 | const query = { 61 | foo: 'bar', 62 | }; 63 | setQuery(interaction, query); 64 | expect(queryMock.calledOnce); 65 | expect(queryMock.calledWith('foo', 0, 'bar')); 66 | }); 67 | }); 68 | 69 | describe('with array and single values', () => { 70 | it('calls the query ffi function for each value', () => { 71 | const queryMock = sinon.stub(); 72 | 73 | const interaction = { 74 | withQuery: queryMock, 75 | } as unknown as ConsumerInteraction; // TODO replace with proper mock 76 | const query = { 77 | foo: 'bar', 78 | baz: ['bat', 'foo'], 79 | }; 80 | setQuery(interaction, query); 81 | expect(queryMock.calledThrice); 82 | expect(queryMock.calledWith('foo', 0, 'bar')); 83 | expect(queryMock.calledWith('baz', 0, 'bat')); 84 | expect(queryMock.calledWith('baz', 1, 'foo')); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/httpPact/tracing.ts: -------------------------------------------------------------------------------- 1 | import http, { RequestOptions, ClientRequest, IncomingMessage } from 'http'; 2 | 3 | import logger from '../common/logger'; 4 | 5 | export const traceHttpInteractions = (): void => { 6 | const originalRequest = http.request; 7 | http.request = ( 8 | options: RequestOptions | string | URL, 9 | cb: RequestOptions | ((res: IncomingMessage) => void) | undefined 10 | ): ClientRequest => { 11 | if (typeof options === 'string' || options instanceof URL) { 12 | throw new Error( 13 | 'invoking traced requests with a string or a URL first argument is not supported' 14 | ); 15 | } 16 | if (typeof cb !== 'function') { 17 | throw new Error( 18 | 'invoking traced requests with a non-function second argument is not supported' 19 | ); 20 | } 21 | const requestBodyChunks: Buffer[] = []; 22 | const responseBodyChunks: Buffer[] = []; 23 | const hijackedCallback = (res: IncomingMessage) => { 24 | logger.trace( 25 | `outgoing request: ${JSON.stringify({ 26 | ...options, 27 | body: Buffer.concat(requestBodyChunks).toString('utf8'), 28 | })}` 29 | ); 30 | if (cb) { 31 | cb(res); 32 | } 33 | }; 34 | const clientRequest: ClientRequest = originalRequest( 35 | options, 36 | hijackedCallback 37 | ); 38 | const oldWrite = clientRequest.end.bind(clientRequest); 39 | clientRequest.end = (chunk: Parameters[0]) => { 40 | requestBodyChunks.push(Buffer.from(chunk)); 41 | return oldWrite(chunk); 42 | }; 43 | 44 | clientRequest.on('response', (incoming: IncomingMessage) => { 45 | incoming.on('readable', () => { 46 | responseBodyChunks.push(Buffer.from(incoming.read())); 47 | }); 48 | incoming.on('end', () => { 49 | logger.trace( 50 | `response: ${JSON.stringify({ 51 | body: Buffer.concat(responseBodyChunks).toString('utf8'), 52 | headers: incoming.headers, 53 | statusCode: incoming.statusCode, 54 | })}` 55 | ); 56 | }); 57 | }); 58 | return clientRequest; 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pact module meta package. 3 | * @module Pact 4 | */ 5 | 6 | /** 7 | * Exposes {@link Pact} 8 | * @memberof Pact 9 | * @static 10 | */ 11 | 12 | /** 13 | * Exposes {@link Matchers} 14 | * To avoid polluting the root module's namespace, re-export 15 | * Matchers as its own module 16 | * @memberof Pact 17 | * @static 18 | */ 19 | import * as MatchersStar from './dsl/matchers'; 20 | 21 | export const Matchers = MatchersStar; 22 | 23 | export { InterfaceToTemplate } from './dsl/matchers'; 24 | 25 | export { Pact } from './httpPact'; 26 | 27 | /** 28 | * Exposes {@link MessageConsumerPact} 29 | * @memberof Pact 30 | * @static 31 | */ 32 | export * from './messageConsumerPact'; 33 | 34 | /** 35 | * Exposes {@link MessageProviderPact} 36 | * @memberof Pact 37 | * @static 38 | */ 39 | export { 40 | MessageProviderPact, 41 | providerWithMetadata, 42 | } from './messageProviderPact'; 43 | 44 | /** 45 | * Exposes {@link Message} 46 | * @memberof Pact 47 | * @static 48 | */ 49 | export * from './dsl/message'; 50 | 51 | /** 52 | * Exposes {@link Verifier} 53 | * @memberof Pact 54 | * @static 55 | */ 56 | export * from './dsl/verifier/verifier'; 57 | export { VerifierOptions } from './dsl/verifier/types'; 58 | 59 | /** 60 | * Exposes {@link GraphQL} 61 | * @memberof Pact 62 | * @static 63 | */ 64 | export * from './dsl/graphql'; 65 | /** 66 | * Exposes {@link ApolloGraphQL} 67 | * @memberof Pact 68 | * @static 69 | */ 70 | export * from './dsl/apolloGraphql'; 71 | 72 | /** 73 | * Exposes {@link Interaction} 74 | * @memberof Pact 75 | * @static 76 | */ 77 | export * from './dsl/interaction'; 78 | 79 | /** 80 | * Exposes {@link MockService} 81 | * @memberof Pact 82 | * @static 83 | */ 84 | export * from './dsl/mockService'; 85 | 86 | export * from './v3'; 87 | 88 | export * from './v4'; 89 | 90 | /** 91 | * Exposes {@link PactOptions} 92 | * @memberof Pact 93 | * @static 94 | */ 95 | export * from './dsl/options'; 96 | -------------------------------------------------------------------------------- /src/v3/ffi.ts: -------------------------------------------------------------------------------- 1 | import { forEachObjIndexed } from 'ramda'; 2 | import { ConsumerInteraction } from '@pact-foundation/pact-core'; 3 | import { Matcher, TemplateHeaders, V3Request, V3Response } from './types'; 4 | import * as MatchersV3 from './matchers'; 5 | 6 | type TemplateHeaderArrayValue = string[] | Matcher[]; 7 | 8 | export const setRequestDetails = ( 9 | interaction: ConsumerInteraction, 10 | req: V3Request 11 | ): void => { 12 | interaction.withRequest( 13 | req.method, 14 | MatchersV3.matcherValueOrString(req.path) 15 | ); 16 | forEachObjIndexed((v, k) => { 17 | if (Array.isArray(v)) { 18 | (v as TemplateHeaderArrayValue).forEach((header, index) => { 19 | interaction.withRequestHeader( 20 | k, 21 | index, 22 | MatchersV3.matcherValueOrString(header) 23 | ); 24 | }); 25 | } else { 26 | interaction.withRequestHeader(k, 0, MatchersV3.matcherValueOrString(v)); 27 | } 28 | }, req.headers); 29 | 30 | forEachObjIndexed((v, k) => { 31 | if (Array.isArray(v)) { 32 | (v as unknown[]).forEach((vv, i) => { 33 | interaction.withQuery(k, i, MatchersV3.matcherValueOrString(vv)); 34 | }); 35 | } else { 36 | interaction.withQuery(k, 0, MatchersV3.matcherValueOrString(v)); 37 | } 38 | }, req.query); 39 | }; 40 | 41 | export const setResponseDetails = ( 42 | interaction: ConsumerInteraction, 43 | res: V3Response 44 | ): void => { 45 | interaction.withStatus(res.status); 46 | 47 | forEachObjIndexed((v, k) => { 48 | if (Array.isArray(v)) { 49 | (v as TemplateHeaderArrayValue).forEach((header, index) => { 50 | interaction.withResponseHeader( 51 | k, 52 | index, 53 | MatchersV3.matcherValueOrString(header) 54 | ); 55 | }); 56 | } else { 57 | interaction.withResponseHeader(k, 0, MatchersV3.matcherValueOrString(v)); 58 | } 59 | }, res.headers); 60 | }; 61 | 62 | // TODO: this might need to consider an array of values 63 | export const contentTypeFromHeaders = ( 64 | headers: TemplateHeaders | undefined, 65 | defaultContentType: string 66 | ): string => { 67 | let contentType: string | Matcher = defaultContentType; 68 | forEachObjIndexed((v, k) => { 69 | if (`${k}`.toLowerCase() === 'content-type') { 70 | contentType = MatchersV3.matcherValueOrString(v); 71 | } 72 | }, headers || {}); 73 | 74 | return contentType; 75 | }; 76 | -------------------------------------------------------------------------------- /src/v3/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pact'; 2 | export * from './types'; 3 | 4 | /** 5 | * Exposes {@link MatchersV3} 6 | * @memberof Pact 7 | * @static 8 | */ 9 | export * as MatchersV3 from './matchers'; 10 | 11 | /** 12 | * Exposes {@link xml} 13 | * @memberof Pact 14 | * @static 15 | */ 16 | export * from './xml/xmlBuilder'; 17 | export * from './xml/xmlElement'; 18 | export * from './xml/xmlNode'; 19 | export * from './xml/xmlText'; 20 | -------------------------------------------------------------------------------- /src/v3/xml/xmlBuilder.ts: -------------------------------------------------------------------------------- 1 | import { XmlElement } from './xmlElement'; 2 | 3 | /** 4 | * XML Builder class for constructing XML documents with matchers 5 | */ 6 | export class XmlBuilder { 7 | private root: XmlElement; 8 | 9 | constructor( 10 | private version: string, 11 | private charset: string, 12 | rootElement: string 13 | ) { 14 | this.root = new XmlElement(rootElement); 15 | } 16 | 17 | public build(callback: (doc: XmlElement) => void): string { 18 | callback(this.root); 19 | 20 | return JSON.stringify(this); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/v3/xml/xmlElement.ts: -------------------------------------------------------------------------------- 1 | import { isMatcher } from '../matchers'; 2 | import { Matcher } from '../types'; 3 | import { XmlNode } from './xmlNode'; 4 | import { XmlText } from './xmlText'; 5 | 6 | export type XmlAttributes = Map; 7 | export type XmlCallback = (n: XmlElement) => void; 8 | 9 | const modifyElementWithCallback = (el: XmlElement, cb?: XmlCallback) => { 10 | if (cb) { 11 | cb(el); 12 | } 13 | }; 14 | export class XmlElement extends XmlNode { 15 | private attributes: XmlAttributes; 16 | 17 | children: XmlNode[] = []; 18 | 19 | constructor(public name: string) { 20 | super(); 21 | } 22 | 23 | public setName(name: string): XmlElement { 24 | this.name = name; 25 | 26 | return this; 27 | } 28 | 29 | public setAttributes(attributes: XmlAttributes): XmlElement { 30 | this.attributes = attributes; 31 | 32 | return this; 33 | } 34 | 35 | /** 36 | * Creates a new element with the given name and attributes and then sets it's text content (can be a matcher) 37 | * @param name Element name 38 | * @param attributes Map of element attributes 39 | * @param arg Callback to configure the new element, or text content to create the new element with (can be a matcher) 40 | */ 41 | public appendElement( 42 | name: string, 43 | attributes: XmlAttributes, 44 | arg?: string | XmlCallback | Matcher 45 | ): XmlElement { 46 | const el = new XmlElement(name).setAttributes(attributes); 47 | if (arg) { 48 | if (typeof arg !== 'function') { 49 | el.appendText(arg); 50 | } else { 51 | modifyElementWithCallback(el, arg); 52 | } 53 | } 54 | this.children.push(el); 55 | 56 | return this; 57 | } 58 | 59 | public appendText(content: string | Matcher): XmlElement { 60 | if (typeof content === 'object' && content['pact:matcher:type']) { 61 | this.children.push( 62 | new XmlText( 63 | isMatcher(content) && 64 | 'value' in content && 65 | content.value !== undefined && 66 | typeof content.value === 'string' 67 | ? content.value 68 | : '', 69 | content 70 | ) 71 | ); 72 | } else { 73 | this.children.push(new XmlText(content.toString())); 74 | } 75 | return this; 76 | } 77 | 78 | public eachLike( 79 | name: string, 80 | attributes: XmlAttributes, 81 | cb?: XmlCallback, 82 | options: EachLikeOptions = { examples: 1 } 83 | ): XmlElement { 84 | const el = new XmlElement(name).setAttributes(attributes); 85 | modifyElementWithCallback(el, cb); 86 | this.children.push({ 87 | 'pact:matcher:type': 'type', 88 | value: el, 89 | examples: options.examples, 90 | }); 91 | 92 | return this; 93 | } 94 | } 95 | 96 | interface EachLikeOptions { 97 | min?: number; 98 | max?: number; 99 | examples?: number; 100 | } 101 | -------------------------------------------------------------------------------- /src/v3/xml/xmlNode.ts: -------------------------------------------------------------------------------- 1 | export class XmlNode {} 2 | -------------------------------------------------------------------------------- /src/v3/xml/xmlText.ts: -------------------------------------------------------------------------------- 1 | import { Matcher } from '../types'; 2 | import { XmlNode } from './xmlNode'; 3 | 4 | export class XmlText extends XmlNode { 5 | constructor( 6 | public content: string, 7 | public matcher?: Matcher 8 | ) { 9 | super(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/v4/index.ts: -------------------------------------------------------------------------------- 1 | import { ConsumerPact, makeConsumerPact } from '@pact-foundation/pact-core'; 2 | import { UnconfiguredInteraction } from './http'; 3 | import { PactV4Options, V4UnconfiguredInteraction } from './http/types'; 4 | import { V4ConsumerPact } from './types'; 5 | import { version as pactPackageVersion } from '../../package.json'; 6 | import { V4UnconfiguredSynchronousMessage } from './message/types'; 7 | import { UnconfiguredSynchronousMessage } from './message'; 8 | import { SpecificationVersion } from '../v3'; 9 | 10 | export class PactV4 implements V4ConsumerPact { 11 | private pact: ConsumerPact; 12 | 13 | constructor(private opts: PactV4Options) { 14 | this.setup(); 15 | this.pact.addMetadata('pact-js', 'version', pactPackageVersion); 16 | } 17 | 18 | setup(): void { 19 | this.pact = makeConsumerPact( 20 | this.opts.consumer, 21 | this.opts.provider, 22 | this.opts.spec ?? SpecificationVersion.SPECIFICATION_VERSION_V4, 23 | this.opts.logLevel ?? 'info' 24 | ); 25 | } 26 | 27 | addInteraction(): V4UnconfiguredInteraction { 28 | return new UnconfiguredInteraction( 29 | this.pact, 30 | this.pact.newInteraction(''), 31 | this.opts, 32 | () => { 33 | // This function needs to be called if the PactV4 object is to be re-used (commonly expected by users) 34 | // Because of the type-state model used here, it's a bit awkward as we need to thread this through 35 | // to children, ultimately to be called on the "executeTest" stage. 36 | this.setup(); 37 | } 38 | ); 39 | } 40 | 41 | addSynchronousInteraction( 42 | description: string 43 | ): V4UnconfiguredSynchronousMessage { 44 | return new UnconfiguredSynchronousMessage( 45 | this.pact, 46 | this.pact.newSynchronousMessage(description), 47 | this.opts, 48 | () => { 49 | // This function needs to be called if the PactV4 object is to be re-used (commonly expected by users) 50 | // Because of the type-state model used here, it's a bit awkward as we need to thread this through 51 | // to children, ultimately to be called on the "executeTest" stage. 52 | this.setup(); 53 | } 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/v4/types.ts: -------------------------------------------------------------------------------- 1 | import { V4UnconfiguredInteraction } from './http/types'; 2 | import { V4UnconfiguredSynchronousMessage } from './message/types'; 3 | 4 | export interface V4ConsumerPact { 5 | addInteraction(): V4UnconfiguredInteraction; 6 | addSynchronousInteraction( 7 | description: string 8 | ): V4UnconfiguredSynchronousMessage; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "sourceMap": true, 6 | "noLib": false, 7 | "noImplicitReturns": true, 8 | "noImplicitAny": false, 9 | "noImplicitThis": true, 10 | "resolveJsonModule": true, 11 | "strictNullChecks": true, 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "noEmitOnError": true, 15 | "emitDecoratorMetadata": true, 16 | "declaration": true, 17 | "experimentalDecorators": true, 18 | "target": "es5", 19 | "lib": ["es2016", "es2017", "dom", "esnext.asynciterable", "esnext"] 20 | }, 21 | "include": ["src", "test", "native/index.node.d.ts", "v3"], 22 | "exclude": ["./node_modules/**", "dist", "examples"] 23 | } 24 | --------------------------------------------------------------------------------