├── .codeclimate.yml ├── .github ├── FUNDING.yml ├── dependabot.yml ├── issue_template.md ├── pact-small.svg └── workflows │ ├── golangci-lint.yml │ ├── release.yml │ ├── smartbear-issue-label-added.yml │ ├── test.yml │ ├── triage.yml │ ├── trigger_pact_docs_update.yml │ └── update-ffi.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile.alpine ├── Dockerfile.debian ├── LICENSE ├── MIGRATION.md ├── Makefile ├── README-old.md ├── README.md ├── RELEASING.md ├── command ├── check.go ├── install.go ├── root.go ├── root_test.go ├── version.go └── version_test.go ├── consumer ├── http.go ├── http_v2.go ├── http_v3.go ├── http_v4.go ├── http_v4_test.go ├── interaction.go ├── interaction_test.go ├── pact_plugin.proto ├── request.go └── response.go ├── doc.go ├── docker-compose.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 ├── examples.md ├── messages.md ├── plugins.md ├── provider.md └── troubleshooting.md ├── examples ├── .gitignore ├── README.md ├── avro │ ├── avro_consumer_test.go │ ├── avro_provider_test.go │ ├── codec.go │ ├── user.avsc │ └── user.go ├── basic_test.go ├── consumer_v2_test.go ├── consumer_v3_test.go ├── consumer_v4_test.go ├── grpc │ ├── common.go │ ├── grpc_consumer_test.go │ ├── grpc_provider_test.go │ └── routeguide │ │ ├── data │ │ ├── data.go │ │ └── x509 │ │ │ ├── README.md │ │ │ ├── ca_cert.pem │ │ │ ├── ca_key.pem │ │ │ ├── client_ca_cert.pem │ │ │ ├── client_ca_key.pem │ │ │ ├── client_cert.pem │ │ │ ├── client_key.pem │ │ │ ├── create.sh │ │ │ ├── openssl.cnf │ │ │ ├── server_cert.pem │ │ │ └── server_key.pem │ │ ├── route_guide.pb.go │ │ ├── route_guide.proto │ │ ├── route_guide_grpc.pb.go │ │ └── server │ │ └── server.go ├── plugin │ ├── consumer_plugin_test.go │ └── provider_plugin_test.go ├── protobuf-message │ ├── protobuf_consumer_test.go │ └── protobuf_provider_test.go ├── provider_test.go └── types │ ├── repository.go │ └── user_service.go ├── go.mod ├── go.sum ├── installer ├── installer.go └── installer_test.go ├── internal ├── checker │ └── checker.go └── native │ ├── c_types_unix.go │ ├── c_types_win.go │ ├── io.pact.plugin │ ├── plugin.pb.go │ └── plugin_grpc.pb.go │ ├── lib.go │ ├── message_server.go │ ├── message_server_test.go │ ├── mismatch.go │ ├── mock_server.go │ ├── mock_server_test.go │ ├── pact.h │ ├── pact_plugin.proto │ ├── plugin.go │ ├── plugin.pb.go │ ├── plugin_grpc.pb.go │ ├── verifier.go │ └── verifier_test.go ├── log └── log.go ├── main.go ├── make └── config.mk ├── matchers ├── matcher.go ├── matcher_test.go └── matcher_v3.go ├── message ├── message.go ├── v3 │ ├── asynchronous_message.go │ └── message.go ├── v4 │ ├── asynchronous_message.go │ ├── asynchronous_message_test.go │ ├── message.go │ ├── synchronous_message.go │ ├── synchronous_message_test.go │ └── transport.go └── verifier.go ├── models ├── pact_file.go └── provider_state.go ├── provider ├── consumer_version_selector.go ├── transport.go ├── verifier.go ├── verify_request.go └── verify_request_test.go ├── proxy ├── http.go └── http_test.go ├── renovate.json ├── scripts ├── build.ps1 ├── create-pr-to-update-pact-ffi.sh ├── dispatch-ffi-released.sh ├── examples_consumer.ps1 ├── examples_provider.ps1 ├── install-cli.sh ├── lib ├── pact.ps1 ├── release.sh └── unit.ps1 ├── utils ├── datetime.go ├── json_utils.go ├── port.go └── port_test.go └── version └── version.go /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | fixme: 4 | enabled: true 5 | govet: 6 | enabled: true 7 | golint: 8 | enabled: true 9 | gofmt: 10 | enabled: true 11 | 12 | ratings: 13 | paths: 14 | - "**.go" 15 | 16 | exclude_paths: [] 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: pact-foundation 4 | custom: ['https://pactflow.io'] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | 15 | # Maintain dependencies for Composer 16 | - package-ecosystem: "gomod" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Software versions 2 | 3 | * **OS**: e.g. Mac OSX 10.11.5 4 | * **Consumer Pact library**: e.g. Pact go v0.0.8 5 | * **Provider Pact library**: e.g. pact-jvm-provider-maven_2.11 v 3.3.8 6 | * **Golang Version**: `go version` 7 | * **Golang environment**: Provide output of `go env` 8 | 9 | ## Expected behaviour 10 | 11 | TBC 12 | 13 | ## Actual behaviour 14 | 15 | TBC 16 | 17 | ## Steps to reproduce 18 | 19 | Provide a repository, gist or reproducable code snippet so that we can test the problem. You may also want to adapt one of the examples in the repository to demonstrate the problem. 20 | 21 | ## Relevent log files 22 | 23 | Please ensure you set logging to `DEBUG` and attach any relevant log files here (or link from a gist). -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v2* 6 | branches: 7 | - master 8 | - main 9 | pull_request: 10 | jobs: 11 | golangci: 12 | name: lint 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | go-version: [ 17 | # 1.19.x, # Ended 06 Sep 2023 18 | # 1.20.x, # Ended 06 Feb 2024 19 | # 1.21.x, # Ended 13 Aug 2024 20 | # 1.22.x, # Ended 11 Feb 2025 21 | 1.23.x, 22 | 1.24.x, 23 | ] 24 | os: [ubuntu-latest] 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | - name: Install Go 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | - name: golangci-lint 34 | uses: golangci/golangci-lint-action@v6 35 | with: 36 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 37 | version: v1.64 38 | 39 | # Optional: working directory, useful for monorepos 40 | # working-directory: somedir 41 | 42 | # Optional: golangci-lint command line arguments. 43 | # ignore the lib.go file as it only contains cgo annotations 44 | args: --exclude-files internal/native/lib.go --timeout 2m 45 | 46 | # Optional: show only new issues if it's a pull request. The default value is `false`. 47 | # only-new-issues: true 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | goreleaser: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - 19 | name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.23.0 23 | - 24 | name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v6 26 | with: 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUBRELEASETOKEN }} -------------------------------------------------------------------------------- /.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/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | inputs: 12 | SKIP_ALPINE_PROVIDER_TESTS: 13 | description: 'Skip alpine provider tests' 14 | required: false 15 | default: 'false' 16 | 17 | env: 18 | PACT_BROKER_BASE_URL: https://testdemo.pactflow.io 19 | PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }} 20 | REACT_APP_API_BASE_URL: http://localhost:8080 21 | APP_SHA: ${{ github.sha }} 22 | APP_REF: ${{ github.ref }} 23 | LD_LIBRARY_PATH: /tmp 24 | PACT_GO_LIB_DOWNLOAD_PATH: /tmp 25 | LOG_LEVEL: debug 26 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | jobs: 29 | test: 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | go-version: [ # https://endoflife.date/go 34 | # 1.17.x, # Ended 02 Aug 2022 35 | # 1.18.x, # Ended 01 Feb 2023 36 | # 1.19.x, # Ended 06 Sep 2023 37 | # 1.20.x, # Ended 06 Feb 2024 38 | # 1.21.x, # Ended 13 Aug 2024 39 | # 1.22.x, # Ended 11 Feb 2025 40 | 1.23.x, 41 | 1.24.x, 42 | ] 43 | os: [ubuntu-latest, macos-13, macos-14, windows-latest] 44 | runs-on: ${{ matrix.os }} 45 | steps: 46 | - name: Checkout code 47 | uses: actions/checkout@v4 48 | - name: Install Go 49 | uses: actions/setup-go@v5 50 | with: 51 | go-version: ${{ matrix.go-version }} 52 | - uses: actions/setup-java@v4 # Needed for the Avro example 53 | with: 54 | distribution: 'zulu' 55 | java-version: '17' 56 | - if: matrix.os == 'macos-14' 57 | run: brew install protobuf 58 | - name: Test 59 | if: matrix.os == 'ubuntu-latest' 60 | run: APP_BRANCH=${APP_REF:11} DOCKER_GATEWAY_HOST=172.17.0.1 DOCKER_HOST_HTTP="http://172.17.0.1" make 61 | - name: Set CGO_LDFLAGS / pact_ffi lib on PATH 62 | if: matrix.os == 'windows-latest' 63 | run: | 64 | "CGO_LDFLAGS=-L$env:TMP" >> $env:GITHUB_ENV 65 | "$env:TMP" >> $env:GITHUB_PATH 66 | - name: Test (unit) 67 | if: matrix.os != 'ubuntu-latest' 68 | run: make test 69 | - name: Test (pact) 70 | if: matrix.os != 'ubuntu-latest' 71 | run: make pact_local 72 | - name: Install goveralls 73 | if: matrix.os != 'windows-latest' 74 | run: go install github.com/mattn/goveralls@latest 75 | - name: Send coverage 76 | if: matrix.os != 'windows-latest' 77 | run: goveralls -coverprofile=coverage.txt -service=github -parallel 78 | - uses: actions/upload-artifact@v4 79 | with: 80 | name: logs-${{ github.job }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.go-version }}-${{ matrix.os }}.zip 81 | path: ~/.pact/plugins/**/plugin.log 82 | if: ${{ always() }} 83 | 84 | test-containers: 85 | continue-on-error: ${{ matrix.experimental }} 86 | runs-on: ${{ matrix.os }} 87 | name: ${{ matrix.os }}-${{ matrix.variant }}-${{ matrix.go-version }}-test-container 88 | strategy: 89 | fail-fast: false 90 | matrix: 91 | os: [ubuntu-latest, ubuntu-24.04-arm] 92 | go-version: ["1.23", "1.24"] 93 | variant: [debian] 94 | experimental: [false] 95 | include: 96 | - variant: alpine 97 | go-version: 1.23 98 | experimental: true 99 | os: ubuntu-latest 100 | - variant: alpine 101 | go-version: 1.24 102 | experimental: true 103 | os: ubuntu-latest 104 | - variant: alpine 105 | go-version: 1.23 106 | experimental: true 107 | os: ubuntu-24.04-arm 108 | - variant: alpine 109 | go-version: 1.24 110 | experimental: true 111 | os: ubuntu-24.04-arm 112 | steps: 113 | - uses: actions/checkout@v4 114 | 115 | - name: Test dockerfile 116 | run: | 117 | if [[ "${{ matrix.variant }}" == "alpine" ]]; then 118 | export SKIP_PROVIDER_TESTS=true 119 | if [[ "${{ github.event.inputs.SKIP_ALPINE_PROVIDER_TESTS }}" == "false" ]]; then 120 | export SKIP_PROVIDER_TESTS=false 121 | fi 122 | fi 123 | make docker_test_all 124 | env: 125 | GO_VERSION: ${{ matrix.go-version }} 126 | IMAGE_VARIANT: ${{ matrix.variant }} 127 | 128 | finish: 129 | needs: [test,test-containers] 130 | runs-on: ubuntu-latest 131 | steps: 132 | - name: Coveralls Finished 133 | uses: coverallsapp/github-action@main 134 | with: 135 | github-token: ${{ secrets.GITHUB_TOKEN }} 136 | parallel-finished: true 137 | -------------------------------------------------------------------------------- /.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 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - '**.md' 10 | 11 | jobs: 12 | run: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Trigger docs.pact.io update workflow 16 | run: | 17 | curl -X POST https://api.github.com/repos/pact-foundation/docs.pact.io/dispatches \ 18 | -H 'Accept: application/vnd.github.everest-preview+json' \ 19 | -H "Authorization: Bearer $GITHUB_TOKEN" \ 20 | -d '{"event_type": "pact-go-docs-updated"}' 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }} 23 | -------------------------------------------------------------------------------- /.github/workflows/update-ffi.yml: -------------------------------------------------------------------------------- 1 | name: Update Pact FFI Library 2 | 3 | on: 4 | repository_dispatch: 5 | types: 6 | - pact-ffi-released 7 | 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - run: | 15 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 16 | git config --global user.name "${GITHUB_ACTOR}" 17 | git config pull.ff only 18 | 19 | - run: scripts/create-pr-to-update-pact-ffi.sh ${{ github.event.client_payload.version }} 20 | env: 21 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | 6 | # TODO: bring this back out and remove files from history before merging into master 7 | # *.so 8 | # *.dylib 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | pkg 30 | bin 31 | dist 32 | build 33 | output 34 | pact-mock-service 35 | pact-provider-verifier 36 | 37 | *.out 38 | *.iml 39 | *.idea 40 | *.bak 41 | _* 42 | *.log 43 | pact-go 44 | pacts 45 | examples/pacts/*.json 46 | logs 47 | tmp 48 | coverage.txt 49 | 50 | # IDE 51 | .vscode 52 | 53 | # Dependencies 54 | pact -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod download 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | checksum: 15 | name_template: 'checksums.txt' 16 | snapshot: 17 | name_template: "{{ .Tag }}-next" 18 | changelog: 19 | sort: asc 20 | filters: 21 | exclude: 22 | - '^docs:' 23 | - '^test:' 24 | - '^chore:' 25 | - '^wip:' 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at pact-support@googlegroups.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pact go 2 | 3 | ## Raising defects 4 | 5 | Before raising an issue, make sure you have checked the open and closed issues to see if an answer is provided there. 6 | There may also be an answer to your question on [stackoverflow](https://stackoverflow.com/questions/tagged/pact). 7 | 8 | Please provide the following information with your issue to enable us to respond as quickly as possible. 9 | 10 | 1. The relevant versions of the packages you are using. 11 | 1. The steps to recreate your issue. 12 | 1. An executable code example where possible. You can fork this repository and modify the e2e [examples](https://github.com/pact-foundation/pact-go/blob/master/examples) to quickly recreate your issue. 13 | 14 | You can run the E2E tests by: 15 | 16 | ```sh 17 | make fake_pact # Run the Pact tests - consumer + provider 18 | ``` 19 | 20 | ## New features / changes 21 | 22 | 1. Fork it 23 | 1. Create your feature branch (git checkout -b my-new-feature) 24 | 1. Commit your changes (git commit -am 'Add some feature') 25 | 1. Push to the branch (git push origin my-new-feature) 26 | 1. Create new Pull Request 27 | 28 | ### Commit messages 29 | 30 | Pact Go uses the [Conventional Changelog](https://github.com/bcoe/conventional-changelog-standard/blob/master/convention.md) 31 | message conventions. Please ensure you follow the guidelines. 32 | 33 | If you'd like to get some CLI assistance, getting setup is easy: 34 | 35 | ```shell 36 | npm install commitizen -g 37 | npm i -g cz-conventional-changelog 38 | ``` 39 | 40 | `git cz` to commit and commitizen will guide you. -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- 1 | # Developer documentation 2 | 3 | ## Tooling 4 | 5 | * Docker 6 | * Java (>= 19) - required for the Avro plugin example 7 | 8 | ## Key Branches 9 | 10 | ### `1.x.x` 11 | 12 | The previous major version. Only bug fixes and security updates will be considered. 13 | 14 | ### `master` 15 | 16 | The `2.x.x` release line. Current major version. -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | ARG VERSION=1.23 2 | FROM golang:${VERSION}-alpine 3 | 4 | RUN apk add --no-cache curl gcc musl-dev gzip openjdk17-jre bash protoc protobuf-dev make file 5 | 6 | COPY . /go/src/github.com/pact-foundation/pact-go 7 | 8 | WORKDIR /go/src/github.com/pact-foundation/pact-go 9 | 10 | CMD ["make", "test"] -------------------------------------------------------------------------------- /Dockerfile.debian: -------------------------------------------------------------------------------- 1 | ARG VERSION=latest 2 | FROM golang:${VERSION} 3 | 4 | RUN apt-get update && apt-get install -y openjdk-17-jre file protobuf-compiler 5 | COPY . /go/src/github.com/pact-foundation/pact-go 6 | 7 | WORKDIR /go/src/github.com/pact-foundation/pact-go 8 | 9 | CMD ["make", "test"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Matt Fellows 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 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | ## Major changes from v1.x.x to v2.x.x 4 | 5 | 1. Ruby shared core has been replaced by the [Rust shared core](https://github.com/pact-foundation/pact-reference/tree/master/rust/) 6 | 1. In general, the interface has been reduced in favour of encouraging use of the Pact [CLI tools] 7 | 1. `Publisher` interface has been removed 8 | 1. Ability to create and verify both [v2] and [v3] [specification] pacts 9 | 1. A bunch of new features, including the new v3 specification [matchers and generators](https://github.com/pact-foundation/pact-specification/tree/version-3/), injected provider states and more. 10 | 1. Each test is given a dedicated server and port, simplifying lifecycle events and improving isolation between tests. 11 | 1. You _must_ clear out any pact directory prior to the tests running. Any pact files will be appended to during test execution or may **result** in conflicts. 12 | 13 | ### Package version 14 | 15 | The new package is migratable, with the the `/v2` major version bump 16 | 17 | i.e. `github.com/pact-foundation/pact-go/v2` 18 | 19 | ### Logging / Debugging 20 | 21 | By default, all logging is to stdout. 22 | 23 | You can configure logging with the following environment variables: 24 | 25 | * `PACT_LOG_LEVEL` (it also respects `LOG_LEVEL`) - the level to log at. Must be one of "trace", "debug", "info", "warn", "error" or "off". 26 | * `PACT_LOG_PATH` - a path to a file to redirect logs 27 | 28 | ### Consumer 29 | 30 | #### Primary Interface 31 | 32 | - `dsl.Pact` was the primary interface. This is now replaced with `NewV2Pact` and the `NewV3Pact` methods, which will return you a builder for the corresponding specification. 33 | - `Verify` is now `ExecuteTest` to avoid ambiguity with provider side verification. It also accepts a `*testing.T` argument, to improve error reporting and resolution. 34 | 35 | These are available in consumer package: `"github.com/pact-foundation/pact-go/v2/consumer"` 36 | 37 | The interface now also exposes an improved builder interface that aids discoverability, better types and readability. The previous "all-in-one" request/response style has been preserved (`WithCompleteRequest` and `WithCompleteResponse`), to aid in migration. See the `TestConsumerV2AllInOne` test in the examples to see this in action. 38 | 39 | #### Consumer Test `func` 40 | 41 | The test function provided to `Verify` now accepts a `func(MockServerConfig) error` (previously a `func() error`). 42 | 43 | The test function will receive the details of the mock server configuration, such as the host and port, which may be useful in configuring the test HTTP client, for example. This prevents the need to maintain global state between tests as was previously the case. 44 | 45 | #### Regexes 46 | 47 | You can now use proper POSIX compliant regexes :) 48 | 49 | #### Speed / Parallelism 50 | 51 | the `NewV2Pact` and `NewV3Pact` interfaces are not thread safe (i.e. you shouldn't run parallel tests with them), but you don't need to. Here is one of the examples run on a loop 1000 times, writing to a different file on each run: 52 | 53 | ``` 54 | ➜ examples ✗ time go test -count=1 -tags=consumer -run TestConsumerV2 . 55 | ok github.com/pact-foundation/pact-go/examples 4.269s 56 | go test -count=1 -tags=consumer -run TestConsumerV2 . 4.34s user 1.93s system 113% cpu 5.542 total 57 | ➜ examples ✗ ls -la pacts/*.json | wc -l 58 | 1001 59 | ➜ examples ✗ 60 | ``` 61 | 62 | There is no real need, when in < 5 seconds you can run 1000 consumer pact tests! 63 | 64 | #### Body `content-type` 65 | 66 | The `content-type` is now a mandatory field, to improve matching for bodies of various types. 67 | 68 | #### Binary Payload and Multipart Requests 69 | 70 | Two new builder methods exist for binary/file payloads: 71 | 72 | - `WithBinaryBody` accepts a `[]byte` for matching on binary payloads (e.g. images) 73 | - `WithMultipartFile` accepts a path to file from the file system, and the multipart boundary 74 | 75 | #### Query Strings 76 | 77 | Query strings are now accepted (and [encoded](https://github.com/pact-foundation/pact-specification/tree/version-3/#query-strings-are-stored-as-map-instead-of-strings)) as nested data structures, instead of flat strings and may have an array of values. 78 | 79 | ### Provider 80 | 81 | #### Provider Interface 82 | 83 | - `dsl.Pact` was the primary interface. This is now replaced with `HTTPVerifier` struct and `VerifyProvider` method. 84 | 85 | These are available in provider package: `"github.com/pact-foundation/pact-go/v2/provider"` 86 | 87 | #### Provider State Handlers 88 | 89 | Consumers may now specify multiple provider states. When the provider verifies them, it will invoke the `StateHandler` for each of the states in the interaction. 90 | 91 | The `StateHandler` type has also changed in 3 important ways: 92 | 93 | 1. Provider states may contain [parameters], which may be useful for the state setup (e.g. data for creation of a state) 94 | 1. There is now a `setup` bool, indicating if the state is being setup or torn down. This is helpful for cleaning up state specific items (separate to `AfterEach` and `BeforeEach` which do not have access to the current state information) 95 | 1. If the consumer code uses provider state injected values (citation needed), the provider may return a JSON payload that will be substituted into the incoming request. See this [example](https://pactflow.io/blog/injecting-values-from-provider-states/) for more 96 | 97 | ### Message pacts 98 | 99 | #### Consumer 100 | 101 | - `dsl.Pact` was the primary interface. This is now replaced with `NewMessagePactV3` builder. The `VerifyMessageConsumer` method is now replaced by the method `Verify` on the builder. 102 | 103 | #### Provider 104 | 105 | - `dsl.Pact` was the primary interface. This is now replaced by the `MessageVerifier` struct and the `Verify` method. The main difference is the state handlers, as discussed above. 106 | 107 | These are available in message package: `"github.com/pact-foundation/pact-go/v2/message"` 108 | 109 | [CLI Tools](https://docs.pact.io/implementation_guides/cli/) 110 | [v2](https://github.com/pact-foundation/pact-specification/tree/version-3/) 111 | [v3](https://github.com/pact-foundation/pact-specification/tree/version-2/) 112 | [specification](https://github.com/pact-foundation/pact-specification/) 113 | [parameters](https://github.com/pact-foundation/pact-specification/tree/version-3/#allow-multiple-provider-states-with-parameters) 114 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include make/config.mk 2 | 3 | TEST?=./... 4 | .DEFAULT_GOAL := ci 5 | DOCKER_HOST_HTTP?="http://host.docker.internal" 6 | PACT_CLI="docker run --rm -v ${PWD}:${PWD} -e PACT_BROKER_BASE_URL=$(DOCKER_HOST_HTTP) -e PACT_BROKER_USERNAME -e PACT_BROKER_PASSWORD pactfoundation/pact-cli" 7 | PLUGIN_PACT_PROTOBUF_VERSION=0.5.4 8 | PLUGIN_PACT_CSV_VERSION=0.0.6 9 | PLUGIN_PACT_MATT_VERSION=0.1.1 10 | PLUGIN_PACT_AVRO_VERSION=0.0.6 11 | 12 | GO_VERSION?=1.23 13 | IMAGE_VARIANT?=debian 14 | ci:: docker deps clean bin test pact 15 | PACT_DOWNLOAD_DIR=/tmp 16 | ifeq ($(OS),Windows_NT) 17 | PACT_DOWNLOAD_DIR=$$TMP 18 | endif 19 | SKIP_RACE?=false 20 | RACE?=-race 21 | ifeq ($(SKIP_RACE),true) 22 | RACE= 23 | endif 24 | # Run the ci target from a developer machine with the environment variables 25 | # set as if it was on Travis CI. 26 | # Use this for quick feedback when playing around with your workflows. 27 | fake_ci: 28 | @CI=true \ 29 | APP_SHA=`git rev-parse --short HEAD`+`date +%s` \ 30 | APP_BRANCH=`git rev-parse --abbrev-ref HEAD` \ 31 | make ci 32 | 33 | # same as above, but just for pact 34 | fake_pact: 35 | @CI=true \ 36 | APP_SHA=`git rev-parse --short HEAD`+`date +%s` \ 37 | APP_BRANCH=`git rev-parse --abbrev-ref HEAD` \ 38 | make pact 39 | 40 | docker: 41 | @echo "--- 🛠 Starting docker" 42 | docker compose up -d 43 | 44 | docker_build: 45 | docker build -f Dockerfile.$(IMAGE_VARIANT) --build-arg GO_VERSION=${GO_VERSION} -t pactfoundation/pact-go-test-$(IMAGE_VARIANT) . 46 | 47 | docker_test: docker_build 48 | docker run \ 49 | -e LOG_LEVEL=INFO \ 50 | -e SKIP_PROVIDER_TESTS=$(SKIP_PROVIDER_TESTS) \ 51 | -e SKIP_RACE=$(SKIP_RACE) \ 52 | --rm \ 53 | -it \ 54 | pactfoundation/pact-go-test-$(IMAGE_VARIANT) \ 55 | /bin/sh -c "make test" 56 | docker_pact: docker_build 57 | docker run \ 58 | -e LOG_LEVEL=INFO \ 59 | -e SKIP_PROVIDER_TESTS=$(SKIP_PROVIDER_TESTS) \ 60 | -e SKIP_RACE=$(SKIP_RACE) \ 61 | --rm \ 62 | pactfoundation/pact-go-test-$(IMAGE_VARIANT) \ 63 | /bin/sh -c "make pact_local" 64 | docker_test_all: docker_build 65 | docker run \ 66 | -e LOG_LEVEL=INFO \ 67 | -e SKIP_PROVIDER_TESTS=$(SKIP_PROVIDER_TESTS) \ 68 | -e SKIP_RACE=$(SKIP_RACE) \ 69 | --rm \ 70 | pactfoundation/pact-go-test-$(IMAGE_VARIANT) \ 71 | /bin/sh -c "make test && make pact_local" 72 | 73 | bin: 74 | go build -o build/pact-go 75 | 76 | clean: 77 | mkdir -p ./examples/pacts 78 | rm -rf build output dist examples/pacts 79 | 80 | deps: download_plugins 81 | 82 | download_plugins: 83 | @echo "--- 🐿 Installing plugins"; \ 84 | if [ -z $$SKIP_PLUGINS ]; then\ 85 | if [ ! -f ~/.pact/bin/pact-plugin-cli ]; then \ 86 | ./scripts/install-cli.sh; \ 87 | else \ 88 | echo "--- 🐿 Pact CLI already installed"; \ 89 | fi; \ 90 | if [ ! -f ~/.pact/plugins/protobuf-$(PLUGIN_PACT_PROTOBUF_VERSION)/pact-protobuf-plugin ]; then \ 91 | ~/.pact/bin/pact-plugin-cli -y install https://github.com/pactflow/pact-protobuf-plugin/releases/tag/v-$(PLUGIN_PACT_PROTOBUF_VERSION); \ 92 | else \ 93 | echo "--- 🐿 Pact protobuf-$(PLUGIN_PACT_PROTOBUF_VERSION) already installed"; \ 94 | fi; \ 95 | if [ ! -f ~/.pact/plugins/csv-$(PLUGIN_PACT_CSV_VERSION)/pact-csv-plugin ]; then \ 96 | ~/.pact/bin/pact-plugin-cli -y install https://github.com/pact-foundation/pact-plugins/releases/tag/csv-plugin-$(PLUGIN_PACT_CSV_VERSION); \ 97 | else \ 98 | echo "--- 🐿 Pact csv-$(PLUGIN_PACT_CSV_VERSION) already installed"; \ 99 | fi; \ 100 | if [ ! -f ~/.pact/plugins/matt-$(PLUGIN_PACT_MATT_VERSION)/matt ]; then \ 101 | ~/.pact/bin/pact-plugin-cli -y install https://github.com/mefellows/pact-matt-plugin/releases/tag/v$(PLUGIN_PACT_MATT_VERSION); \ 102 | else \ 103 | echo "--- 🐿 Pact matt-$(PLUGIN_PACT_MATT_VERSION) already installed"; \ 104 | fi; \ 105 | if [ ! -f ~/.pact/plugins/avro-$(PLUGIN_PACT_AVRO_VERSION)/bin/pact-avro-plugin ]; then \ 106 | ~/.pact/bin/pact-plugin-cli -y install https://github.com/austek/pact-avro-plugin/releases/tag/v$(PLUGIN_PACT_AVRO_VERSION); \ 107 | else \ 108 | echo "--- 🐿 Pact avro-$(PLUGIN_PACT_AVRO_VERSION) already installed"; \ 109 | fi; \ 110 | fi 111 | 112 | cli: 113 | @if [ ! -d pact/bin ]; then\ 114 | echo "--- 🐿 Installing Pact CLI dependencies"; \ 115 | curl -fsSL https://raw.githubusercontent.com/pact-foundation/pact-ruby-standalone/master/install.sh | bash -x; \ 116 | fi 117 | 118 | install: bin 119 | echo "--- 🐿 Installing Pact FFI dependencies" 120 | ./build/pact-go -l DEBUG install --libDir $(PACT_DOWNLOAD_DIR) 121 | 122 | pact: clean install docker 123 | @echo "--- 🔨 Running Pact examples" 124 | go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/v2/examples/... 125 | make publish 126 | go test -v -timeout=30s -tags=provider -count=1 github.com/pact-foundation/pact-go/v2/examples/... 127 | pact_local: clean download_plugins install 128 | @echo "--- 🔨 Running Pact examples" 129 | go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/v2/examples/... 130 | if [ "$(SKIP_PROVIDER_TESTS)" != "true" ]; then \ 131 | SKIP_PUBLISH=true go test -v -timeout=30s -tags=provider -count=1 github.com/pact-foundation/pact-go/v2/examples/...; \ 132 | fi 133 | 134 | publish: 135 | @echo "-- 📃 Publishing pacts" 136 | @"${PACT_CLI}" publish ${PWD}/examples/pacts --consumer-app-version ${APP_SHA} --tag ${APP_BRANCH} --tag prod 137 | 138 | release: 139 | echo "--- 🚀 Releasing it" 140 | "$(CURDIR)/scripts/release.sh" 141 | 142 | ifeq ($(SKIP_PROVIDER_TESTS),true) 143 | PROVIDER_TEST_TAGS= 144 | else 145 | PROVIDER_TEST_TAGS=-tags=provider 146 | endif 147 | 148 | test: deps install 149 | @echo "--- ✅ Running tests" 150 | @if [ -f coverage.txt ]; then rm coverage.txt; fi; 151 | @echo "mode: count" > coverage.txt 152 | @for d in $$(go list ./... | grep -v vendor | grep -v examples); \ 153 | do \ 154 | go test -v $(RACE) -coverprofile=profile.out $(PROVIDER_TEST_TAGS) -covermode=atomic $$d; \ 155 | if [ $$? != 0 ]; then \ 156 | exit 1; \ 157 | fi; \ 158 | if [ -f profile.out ]; then \ 159 | cat profile.out | tail -n +2 >> coverage.txt; \ 160 | rm profile.out; \ 161 | fi; \ 162 | done; \ 163 | go tool cover -func coverage.txt 164 | 165 | 166 | testrace: 167 | go test $(RACE) $(TEST) $(TESTARGS) 168 | 169 | updatedeps: 170 | go get -d -v -p 2 ./... 171 | 172 | .PHONY: install bin default dev test pact updatedeps clean release 173 | 174 | PROTOC ?= $(shell which protoc) 175 | 176 | .PHONY: protos 177 | protos: 178 | @echo "--- 🛠 Compiling Protobufs" 179 | cd ./examples/grpc/routeguide && $(PROTOC) --go_out=paths=source_relative:. \ 180 | --go-grpc_out=paths=source_relative:. ./route_guide.proto 181 | 182 | .PHONY: grpc-test 183 | grpc-test: 184 | rm -rf ./examples/pacts 185 | go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/v2/examples/grpc 186 | go test -v -timeout=30s -tags=provider -count=1 github.com/pact-foundation/pact-go/v2/examples/grpc 187 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | 2 | # Releasing 3 | 4 | Once your changes are in master, the latest release should be set as a draft at https://github.com/pact-foundation/pact-go/releases/. 5 | 6 | Once you've tested that it works as expected: 7 | 8 | 1. Bump version in `command/version.go`. 9 | 1. Run `make release` to generate release notes and release commit. 10 | 1. Edit the release notes at https://github.com/pact-foundation/pact-go/releases/edit/v. 11 | -------------------------------------------------------------------------------- /command/check.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/pact-foundation/pact-go/v2/installer" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var checkCmd = &cobra.Command{ 13 | Use: "check", 14 | Short: "Check required libraries", 15 | Long: "Check the correct version of required libraries", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | setLogLevel(verbose, logLevel) 18 | 19 | // Run the installer 20 | i, err := installer.NewInstaller() 21 | if err != nil { 22 | log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err) 23 | os.Exit(1) 24 | } 25 | 26 | if libDir != "" { 27 | log.Println("[INFO] set lib dir target to", libDir) 28 | i.SetLibDir(libDir) 29 | } 30 | 31 | if err = i.CheckPackageInstall(); err != nil { 32 | log.Println("[DEBUG] error from CheckPackageInstall:", err) 33 | log.Println("[ERROR] Your Pact library installation is out of date. Run `pact-go install` to correct") 34 | os.Exit(1) 35 | } 36 | }, 37 | } 38 | 39 | func init() { 40 | checkCmd.Flags().StringVarP(&libDir, "libDir", "d", "", "Target directory of the library installation") 41 | RootCmd.AddCommand(checkCmd) 42 | } 43 | -------------------------------------------------------------------------------- /command/install.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/pact-foundation/pact-go/v2/installer" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var libDir string 13 | var force bool 14 | var installCmd = &cobra.Command{ 15 | Use: "install", 16 | Short: "Install required libraries", 17 | Long: "Install the correct version of required libraries", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | setLogLevel(verbose, logLevel) 20 | 21 | // Run the installer 22 | i, err := installer.NewInstaller() 23 | 24 | if err != nil { 25 | log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err) 26 | os.Exit(1) 27 | } 28 | 29 | if libDir != "" { 30 | log.Println("[INFO] set lib dir target to", libDir) 31 | i.SetLibDir(libDir) 32 | } 33 | 34 | i.Force(force) 35 | 36 | if err = i.CheckInstallation(); err != nil { 37 | log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err) 38 | os.Exit(1) 39 | } 40 | }, 41 | } 42 | 43 | func init() { 44 | installCmd.Flags().BoolVarP(&force, "force", "f", false, "Force a new installation") 45 | installCmd.Flags().StringVarP(&libDir, "libDir", "d", "", "Target directory to install the library") 46 | RootCmd.AddCommand(installCmd) 47 | } 48 | -------------------------------------------------------------------------------- /command/root.go: -------------------------------------------------------------------------------- 1 | // Package command contains the basic CLI commands to run Pact Go. 2 | package command 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/hashicorp/logutils" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var verbose bool 15 | var logLevel string 16 | 17 | // RootCmd represents the base command when called without any subcommands 18 | var RootCmd = &cobra.Command{ 19 | Use: "pact-go", 20 | Short: "Pact Go makes it easier to work with Pact with Golang projects", 21 | Long: `Pact Go is a utility that wraps a number of external applications into 22 | an idiomatic Golang interface and CLI, providing a mock service and DSL for 23 | the consumer project, and interaction playback and verification for the 24 | service provider project.`, 25 | } 26 | 27 | // Execute adds all child commands to the root command sets flags appropriately. 28 | // This is called by main.main(). It only needs to happen once to the rootCmd. 29 | func Execute() { 30 | if err := RootCmd.Execute(); err != nil { 31 | fmt.Println(err) 32 | os.Exit(-1) 33 | } 34 | } 35 | 36 | func init() { 37 | // Here you will define your flags and configuration settings. 38 | // Cobra supports Persistent Flags, which, if defined here, 39 | // will be global for your application. 40 | RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", true, "verbose output") 41 | RootCmd.PersistentFlags().StringVarP(&logLevel, "logLevel", "l", "INFO", "Set the logging level (DEBUG, INFO, ERROR)") 42 | } 43 | 44 | func setLogLevel(verbose bool, level string) { 45 | filter := &logutils.LevelFilter{ 46 | Levels: []logutils.LogLevel{"DEBUG", "INFO", "ERROR"}, 47 | MinLevel: logutils.LogLevel(level), 48 | Writer: os.Stderr, 49 | } 50 | log.SetOutput(filter) 51 | 52 | if !verbose { 53 | log.SetOutput(io.Discard) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /command/root_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestRootCommand(t *testing.T) { 12 | os.Args = []string{"version"} 13 | err := RootCmd.Execute() 14 | if err != nil { 15 | t.Fatalf("Error: %v", err) 16 | } 17 | 18 | Execute() 19 | } 20 | 21 | func TestRootCommand_LogLevel(t *testing.T) { 22 | res := captureOutput(func() { 23 | setLogLevel(true, "DEBUG") 24 | log.Println("[DEBUG] this should display") 25 | }) 26 | 27 | if !strings.Contains(res, "[DEBUG] this should display") { 28 | t.Fatalf("Expected log message to contain '[DEBUG] this should display' but got '%s'", res) 29 | } 30 | 31 | res = captureOutput(func() { 32 | setLogLevel(true, "INFO") 33 | log.Print("[DEBUG] this should not display") 34 | }) 35 | 36 | if res != "" { 37 | t.Fatalf("Expected log message to be empty but got '%s'", res) 38 | } 39 | 40 | res = captureOutput(func() { 41 | setLogLevel(false, "INFO") 42 | log.Print("[DEBUG] this should not display") 43 | }) 44 | 45 | if res != "" { 46 | t.Fatalf("Expected log message to be empty but got '%s'", res) 47 | } 48 | } 49 | 50 | func captureOutput(action func()) string { 51 | rescueStderr := os.Stderr 52 | r, w, _ := os.Pipe() 53 | os.Stderr = w 54 | 55 | action() 56 | 57 | w.Close() 58 | out, _ := io.ReadAll(r) 59 | os.Stderr = rescueStderr 60 | 61 | return strings.TrimSpace(string(out)) 62 | } 63 | -------------------------------------------------------------------------------- /command/version.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var Version = "v2.4.1" 10 | var versionCmd = &cobra.Command{ 11 | Use: "version", 12 | Short: "Print the version number of Pact Go", 13 | Long: `All software has versions. This is Pact Go's`, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Printf("Pact Go CLI %s", Version) 16 | }, 17 | } 18 | 19 | func init() { 20 | RootCmd.AddCommand(versionCmd) 21 | } 22 | -------------------------------------------------------------------------------- /command/version_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestVersionCommand(t *testing.T) { 9 | os.Args = []string{"version"} 10 | err := versionCmd.Execute() 11 | if err != nil { 12 | t.Fatalf("Error: %v", err) 13 | } 14 | versionCmd.Run(nil, os.Args) 15 | } 16 | -------------------------------------------------------------------------------- /consumer/http_v4_test.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/pact-foundation/pact-go/v2/matchers" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestHttpV4TypeSystem(t *testing.T) { 14 | 15 | p, err := NewV4Pact(MockHTTPProviderConfig{ 16 | Consumer: "consumer", 17 | Provider: "provider", 18 | }) 19 | assert.NoError(t, err) 20 | 21 | err = p.AddInteraction(). 22 | Given("some state"). 23 | UponReceiving("some scenario"). 24 | WithRequest("GET", "/", func(b *V4RequestBuilder) { 25 | b. 26 | Header("Content-Type", S("application/json")). 27 | Header("Authorization", Like("Bearer 1234")). 28 | Query("baz", Regex("bar", "[a-z]+"), Regex("bat", "[a-z]+"), Regex("baz", "[a-z]+")). 29 | JSONBody(Map{ 30 | "id": Like(27), 31 | "name": Like("billy"), 32 | "datetime": Like("2020-01-01'T'08:00:45"), 33 | "lastName": Like("billy"), 34 | }) 35 | }). 36 | WillRespondWith(200, func(b *V4ResponseBuilder) { 37 | b. 38 | Header("Content-Type", Regex("application/json", "application\\/json")). 39 | JSONBody(Map{ 40 | "datetime": Regex("2020-01-01", "[0-9\\-]+"), 41 | "name": S("Billy"), 42 | "lastName": S("Sampson"), 43 | "itemsMin": ArrayMinLike("thereshouldbe3ofthese", 3), 44 | }) 45 | 46 | }). 47 | ExecuteTest(t, func(msc MockServerConfig) error { 48 | // <- normally run the actually test here. 49 | 50 | return nil 51 | }) 52 | assert.Error(t, err) 53 | 54 | dir, _ := os.Getwd() 55 | path := fmt.Sprintf("%s/pact_plugin.proto", strings.ReplaceAll(dir, "\\", "/")) 56 | 57 | err = p.AddInteraction(). 58 | Given("some state"). 59 | UponReceiving("some scenario"). 60 | UsingPlugin(PluginConfig{ 61 | Plugin: "protobuf", 62 | Version: "0.5.4", 63 | }). 64 | WithRequest("GET", "/"). 65 | // WithRequest("GET", "/", func(b *V4InteractionWithPluginRequestBuilder) { 66 | // b.PluginContents("application/protobufs", "") 67 | // // TODO: 68 | // }). 69 | WillRespondWith(200, func(b *V4InteractionWithPluginResponseBuilder) { 70 | b. 71 | Header("Content-Type", S("application/protobufs")). 72 | PluginContents("application/protobufs", ` 73 | { 74 | "pact:proto": "`+path+`", 75 | "pact:message-type": "InitPluginRequest" 76 | } 77 | `) 78 | }). 79 | ExecuteTest(t, func(msc MockServerConfig) error { 80 | // <- normally run the actually test here. 81 | 82 | return nil 83 | }) 84 | assert.Error(t, err) 85 | 86 | } 87 | 88 | var Like = matchers.Like 89 | var EachLike = matchers.EachLike 90 | var Term = matchers.Term 91 | var Regex = matchers.Regex 92 | var HexValue = matchers.HexValue 93 | var Identifier = matchers.Identifier 94 | var IPAddress = matchers.IPAddress 95 | var IPv6Address = matchers.IPv6Address 96 | var Timestamp = matchers.Timestamp 97 | var Date = matchers.Date 98 | var Time = matchers.Time 99 | var UUID = matchers.UUID 100 | 101 | type S = matchers.String 102 | 103 | var ArrayMinLike = matchers.ArrayMinLike 104 | 105 | type Map = matchers.Map 106 | -------------------------------------------------------------------------------- /consumer/interaction.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | mockserver "github.com/pact-foundation/pact-go/v2/internal/native" 9 | "github.com/pact-foundation/pact-go/v2/matchers" 10 | "github.com/pact-foundation/pact-go/v2/models" 11 | ) 12 | 13 | // Interaction is the main implementation of the Pact interface. 14 | type Interaction struct { 15 | // Reference to the native rust handle 16 | interaction *mockserver.Interaction 17 | specificationVersion models.SpecificationVersion 18 | } 19 | 20 | // WithCompleteRequest specifies the details of the HTTP request that will be used to 21 | // confirm that the Provider provides an API listening on the given interface. 22 | // Mandatory. 23 | func (i *Interaction) WithCompleteRequest(request Request) *Interaction { 24 | i.interaction.WithRequest(string(request.Method), request.Path) 25 | 26 | if request.Body != nil { 27 | i.interaction.WithJSONRequestBody(request.Body) 28 | } 29 | 30 | if request.Headers != nil { 31 | i.interaction.WithRequestHeaders(headersMapMatcherToNativeHeaders(request.Headers)) 32 | } 33 | 34 | if request.Query != nil { 35 | i.interaction.WithQuery(headersMapMatcherToNativeHeaders(request.Query)) 36 | } 37 | 38 | return i 39 | } 40 | 41 | // WithCompleteResponse specifies the details of the HTTP response required by the consumer 42 | func (i *Interaction) WithCompleteResponse(response Response) *Interaction { 43 | if response.Body != nil { 44 | i.interaction.WithJSONResponseBody(response.Body) 45 | } 46 | 47 | if response.Headers != nil { 48 | i.interaction.WithResponseHeaders(headersMapMatcherToNativeHeaders(response.Headers)) 49 | } 50 | 51 | i.interaction.WithStatus(response.Status) 52 | 53 | return i 54 | } 55 | 56 | func validateMatchers(version models.SpecificationVersion, obj interface{}) error { 57 | if obj == nil { 58 | return nil 59 | } 60 | 61 | str, err := json.Marshal(obj) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var maybeMatchers map[string]interface{} 67 | err = json.Unmarshal(str, &maybeMatchers) 68 | if err != nil { 69 | // This means the object is not really an object, it's probably a primitive 70 | return nil 71 | } 72 | 73 | invalidMatchers := hasMatcherGreaterThanSpec(version, maybeMatchers) 74 | 75 | if len(invalidMatchers) > 0 { 76 | return fmt.Errorf("the current pact file with specification version %s has attempted to use matchers from a higher spec version: %s", version, strings.Join(invalidMatchers, ", ")) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func hasMatcherGreaterThanSpec(version models.SpecificationVersion, obj map[string]interface{}) []string { 83 | results := make([]string, 0) 84 | 85 | for k, v := range obj { 86 | if k == "pact:specification" && v.(string) > string(version) { 87 | results = append(results, obj["pact:matcher:type"].(string)) 88 | } 89 | 90 | m, ok := v.(map[string]interface{}) 91 | if ok { 92 | results = append(results, hasMatcherGreaterThanSpec(version, m)...) 93 | } 94 | } 95 | 96 | return results 97 | } 98 | 99 | func keyValuesToMapStringArrayInterface(key string, values ...matchers.Matcher) map[string][]interface{} { 100 | q := make(map[string][]interface{}) 101 | for _, v := range values { 102 | q[key] = append(q[key], v) 103 | } 104 | 105 | return q 106 | } 107 | 108 | func headersMatcherToNativeHeaders(headers matchers.HeadersMatcher) map[string][]interface{} { 109 | h := make(map[string][]interface{}) 110 | 111 | for k, v := range headers { 112 | h[k] = make([]interface{}, len(v)) 113 | for i, vv := range v { 114 | h[k][i] = vv 115 | } 116 | } 117 | 118 | return h 119 | } 120 | 121 | func headersMapMatcherToNativeHeaders(headers matchers.MapMatcher) map[string][]interface{} { 122 | h := make(map[string][]interface{}) 123 | 124 | for k, v := range headers { 125 | h[k] = []interface{}{ 126 | v, 127 | } 128 | } 129 | 130 | return h 131 | } 132 | -------------------------------------------------------------------------------- /consumer/interaction_test.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/pact-foundation/pact-go/v2/matchers" 8 | "github.com/pact-foundation/pact-go/v2/models" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestInteraction(t *testing.T) { 13 | 14 | t.Run("validateMatchers for V2 Specification", func(t *testing.T) { 15 | 16 | testCases := []struct { 17 | description string 18 | test interface{} 19 | want error 20 | }{ 21 | { 22 | description: "string body should return nil", 23 | test: "I'm a string", 24 | want: nil, 25 | }, 26 | { 27 | description: "boolean body should return nil", 28 | test: true, 29 | want: nil, 30 | }, 31 | { 32 | description: "numeric body should return nil", 33 | test: 27, 34 | want: nil, 35 | }, 36 | { 37 | description: "v3 matches should error", 38 | test: map[string]interface{}{ 39 | "dateTime": matchers.Regex("2020-01-01", "[0-9\\-]+"), 40 | "name": matchers.S("Billy"), 41 | "superstring": matchers.Includes("foo"), 42 | "nested": map[string]matchers.Matcher{ 43 | "val": matchers.Includes("val"), 44 | }, 45 | }, 46 | want: errors.New("test error"), 47 | }, 48 | { 49 | description: "v2 matches should no terror", 50 | test: map[string]interface{}{ 51 | "dateTime": matchers.Regex("2020-01-01", "[0-9\\-]+"), 52 | "name": matchers.S("Billy"), 53 | "nested": map[string]matchers.Matcher{ 54 | "val": matchers.Regex("val", ".*"), 55 | }, 56 | }, 57 | want: nil, 58 | }, 59 | } 60 | 61 | for _, test := range testCases { 62 | if test.want != nil { 63 | assert.Error(t, validateMatchers(models.V2, test.test), test.description) 64 | } else { 65 | assert.NoError(t, validateMatchers(models.V2, test.test), test.description) 66 | } 67 | } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /consumer/request.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import "github.com/pact-foundation/pact-go/v2/matchers" 4 | 5 | // Request is the default implementation of the Request interface. 6 | type Request struct { 7 | Method string `json:"method"` 8 | Path matchers.Matcher `json:"path"` 9 | Query matchers.MapMatcher `json:"query,omitempty"` 10 | Headers matchers.MapMatcher `json:"headers,omitempty"` 11 | Body interface{} `json:"body,omitempty"` 12 | } 13 | type Method string 14 | -------------------------------------------------------------------------------- /consumer/response.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import "github.com/pact-foundation/pact-go/v2/matchers" 4 | 5 | // Response is the default implementation of the Response interface. 6 | type Response struct { 7 | Status int `json:"status"` 8 | Headers matchers.MapMatcher `json:"headers,omitempty"` 9 | Body interface{} `json:"body,omitempty"` 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | httpbin: 3 | image: kong/httpbin # https://github.com/Kong/httpbin 4 | ports: 5 | - "8000:80" 6 | 7 | postgres: 8 | image: postgres 9 | healthcheck: 10 | test: psql postgres --command "select 1" -U postgres 11 | ports: 12 | - "5432:5432" 13 | environment: 14 | POSTGRES_USER: postgres 15 | POSTGRES_PASSWORD: password 16 | POSTGRES_DB: postgres 17 | 18 | broker_app: 19 | image: pactfoundation/pact-broker:latest 20 | links: 21 | - postgres 22 | ports: 23 | - 80:9292 24 | environment: 25 | PACT_BROKER_BASIC_AUTH_USERNAME: pact_workshop 26 | PACT_BROKER_BASIC_AUTH_PASSWORD: pact_workshop 27 | PACT_BROKER_DATABASE_USERNAME: postgres 28 | PACT_BROKER_DATABASE_PASSWORD: password 29 | PACT_BROKER_DATABASE_HOST: postgres 30 | PACT_BROKER_DATABASE_NAME: postgres 31 | # The Pact Broker provides a healthcheck endpoint which we will use to wait 32 | # for it to become available before starting up 33 | healthcheck: 34 | test: [ "CMD", "wget", "-q", "--tries=1", "--spider", "http://pact_workshop:pact_workshop@localhost:9292/diagnostic/status/heartbeat" ] 35 | interval: 1s 36 | timeout: 2s 37 | retries: 5 38 | depends_on: 39 | postgres: 40 | condition: service_healthy -------------------------------------------------------------------------------- /docs/diagrams/message-consumer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-go/958e9403c08aa8a014ec5b9fca5afc3ed71795df/docs/diagrams/message-consumer.png -------------------------------------------------------------------------------- /docs/diagrams/message-provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-go/958e9403c08aa8a014ec5b9fca5afc3ed71795df/docs/diagrams/message-provider.png -------------------------------------------------------------------------------- /docs/diagrams/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-go/958e9403c08aa8a014ec5b9fca5afc3ed71795df/docs/diagrams/summary.png -------------------------------------------------------------------------------- /docs/diagrams/workshop_step1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 15 |
17 |
19 | Consumer 20 |
21 |
22 |
23 | 24 | Consumer 25 | 26 |
27 |
28 | 29 | 30 | 31 | 33 |
35 |
37 | Provider 38 |
39 |
40 |
41 | 42 | Provider 43 | 44 |
45 |
46 | 47 | 48 | 49 | 51 |
53 |
55 | HTTP 56 |
57 |
58 |
59 | HTTP 60 | 61 |
62 |
63 |
64 |
-------------------------------------------------------------------------------- /docs/diagrams/workshop_step1_failed_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-go/958e9403c08aa8a014ec5b9fca5afc3ed71795df/docs/diagrams/workshop_step1_failed_page.png -------------------------------------------------------------------------------- /docs/diagrams/workshop_step2_failed_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pact-foundation/pact-go/958e9403c08aa8a014ec5b9fca5afc3ed71795df/docs/diagrams/workshop_step2_failed_page.png -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## HTTP APIs 4 | 5 | - [API V2 Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/consumer_v2_test.go) 6 | - [API V3 Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/consumer_v3_test.go) 7 | - [API V4 Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/consumer_v4_test.go) 8 | - [API Provider](https://github.com/pact-foundation/pact-go/tree/master/examples/provider_test.go) 9 | 10 | ## Asynchronous APIs 11 | 12 | - [V3 Message Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/consumer_v3_test.go#L92) 13 | - [V3 Message Provider](https://github.com/pact-foundation/pact-go/tree/master/examples/provider_test.go#L103) 14 | 15 | ## Plugins 16 | 17 | ### gRPC 18 | 19 | - [gRPC Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/grpc/grpc_consumer_test.go) 20 | - [gRPC Provider](https://github.com/pact-foundation/pact-go/tree/master/examples/grpc/grpc_provider_test.go) 21 | 22 | ### Protobuf 23 | 24 | - [Protobuf Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/protobuf-message/protobuf_consumer_test.go) 25 | - [Protobuf Provider](https://github.com/pact-foundation/pact-go/tree/master/examples/protobuf-message/protobuf_provider_test.go) 26 | 27 | ### Avro 28 | 29 | - [Avro Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/avro/avro_consumer_test.go) 30 | - [Avro Provider](https://github.com/pact-foundation/pact-go/tree/master/examples/avro/avro_provider_test.go) 31 | 32 | ### Custom Plugin 33 | 34 | - [Custom plugin Consumer](https://github.com/pact-foundation/pact-go/tree/master/examples/plugin/consumer_plugin_test.go) 35 | - [Custom plugin Provider](https://github.com/pact-foundation/pact-go/tree/master/examples/plugin/provider_plugin_test.go) 36 | 37 | ## Workshop 38 | 39 | - [Pact Workshop Go](https://github.com/pact-foundation/pact-workshop-go) 40 | 41 | ## Community 42 | 43 | > If you have your own pact-go example repository, or blog post, please add it here! 44 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | ## HTTP 4 | 5 | ### Consumer 6 | ### Provider 7 | 8 | ## Messages 9 | 10 | ### Asynchronous 11 | ### Synchronous -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Output Logging 4 | 5 | Pact Go uses a simple log utility ([logutils](https://github.com/hashicorp/logutils)) 6 | to filter log messages. The CLI already contains flags to manage this, 7 | should you want to control log level in your tests, you can set it like so: 8 | 9 | ``` 10 | SetLogLevel("trace") 11 | ``` 12 | 13 | You can also export `LOG_LEVEL=trace` before running a test to increase verbosity. 14 | 15 | ## Library status check 16 | 17 | Pact ships with a CLI that you can also use to check if the tools are up to date. Simply run `pact-go check` - an exit status of `0` is good, `1` or higher is bad. `pact-go install` will also do this, and also install any dependencies if missing. 18 | 19 | You can also opt to have Pact automatically upgrade library version using the function `CheckVersion()`. 20 | 21 | Pact go from 2.0.0-beta-11 onwards, also stores a configuration file in `~/.pact/pact-go.yml` that contains the version information for the current libraries it manages. You should not edit this file, however it has a structure as follows: 22 | 23 | ```yaml 24 | libraries: 25 | libpact_ffi: 26 | libname: libpact_ffi 27 | version: 0.3.2 28 | hash: d6503769896eecbc027815d20aff19c3 29 | ``` 30 | 31 | #### Re-run a specific provider verification test 32 | 33 | Sometimes you want to target a specific test for debugging an issue or some other reason. 34 | 35 | This is easy for the consumer side, as each consumer test can be controlled 36 | within a valid `*testing.T` function, however this is not possible for Provider verification. 37 | 38 | But there is a way! Given an interaction that looks as follows (taken from the message examples): 39 | 40 | ```go 41 | message := pact.AddMessage() 42 | message. 43 | Given("user with id 127 exists"). 44 | ExpectsToReceive("a user"). 45 | WithMetadata(commonHeaders). 46 | WithContent(map[string]interface{}{ 47 | "id": like(127), 48 | "name": "Baz", 49 | "access": eachLike(map[string]interface{}{ 50 | "role": term("admin", "admin|controller|user"), 51 | }, 3), 52 | }). 53 | AsType(&types.User{}) 54 | ``` 55 | 56 | and the function used to run provider verification is `go test -run TestMessageProvider`, you can test the verification of this specific interaction by setting two environment variables `PACT_DESCRIPTION` and `PACT_PROVIDER_STATE` and re-running the command. For example: 57 | 58 | ``` 59 | cd examples/message/provider 60 | PACT_DESCRIPTION="a user" PACT_PROVIDER_STATE="user with id 127 exists" go test -v . 61 | ``` 62 | 63 | ### Verifying APIs with a self-signed certificate 64 | 65 | Supply your own TLS configuration to customise the behaviour of the runtime: 66 | 67 | ```go 68 | _, err := pact.VerifyProvider(t, types.VerifyRequest{ 69 | ProviderBaseURL: "https://localhost:8080", 70 | PactURLs: []string{filepath.ToSlash(fmt.Sprintf("%s/consumer-selfsignedtls.json", pactDir))}, 71 | CustomTLSConfig: &tls.Config{ 72 | RootCAs: getCaCertPool(), // Specify a custom CA pool 73 | // InsecureSkipVerify: true, // Disable SSL verification altogether 74 | }, 75 | }) 76 | ``` 77 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | !pacts 2 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This folder contains a number of examples in different frameworks to demonstrate 4 | how Pact could be used in each. 5 | 6 | Each Provider API currently exposes a single `Login` endpoint at `POST /login/1`, 7 | which the [Consumer](consumer/goconsumer) uses to authenticate a User. 8 | 9 | We test 5 scenarios, highlighting the use of [Provider States](/pact-foundation/pact-go#provider#provider-states), [Hooks](/pact-foundation/pact-go#before-and-after-hooks) and [RequestFilters](/pact-foundation/pact-go#request-filters): 10 | 11 | 1. When the user "jmarie" exists, and we perform a login, we expect an HTTP `200` 12 | 1. When the user "jmarie" does not exists, and we perform a login, we expect an HTTP `404` 13 | 1. When the user "jmarie" is unauthorized, and we perform a login, we expect an HTTP `403` 14 | 1. When the user is authenticated, and we request to get the user 'jmarie', we expect an HTTP `200` 15 | 1. When the user is unauthenticated, and we request to get the user 'jmarie', we expect an HTTP `401` 16 | 17 | # Getting started 18 | 19 | Before any of these tests can be run, ensure Pact Go is installed and run the 20 | daemon in the background: 21 | 22 | ``` 23 | go get ./... 24 | ``` 25 | 26 | ## Providers 27 | 28 | 1. [Mux](mux) 29 | 2. [Gin](gin) 30 | 31 | ## Consumer 32 | 33 | The "Consumer" is a very simple web application exposing a login form and an 34 | authenticated page. In this example it is helpful to assume that the UI (Consumer) 35 | and the API (Provider) are in separated code bases, maintained by separate teams. 36 | 37 | Note that in the Pact testing, we test the `loginHandler` function (an `http.HandlerFunc`) 38 | to test the remote interface and we don't just test the remote interface with 39 | raw http calls. This is important as it means we are testing the remote interface 40 | to our collaborator, not something completely synthetic. 41 | 42 | ``` 43 | cd consumer/goconsumer 44 | go test -v . 45 | ``` 46 | 47 | This will generate a Pact file in `./pacts/jmarie-loginprovider.json`. 48 | 49 | ### Running the Consumer 50 | 51 | Before you can run the consumer make sure one of the providers is 52 | running first. You can then run: 53 | 54 | ``` 55 | go run cmd/web/main.go 56 | ``` 57 | 58 | Hit http://localhost:8081/ in your browser. You can use the username/password 59 | combination of "jmarie" / "issilly" to authenticate. 60 | -------------------------------------------------------------------------------- /examples/avro/avro_consumer_test.go: -------------------------------------------------------------------------------- 1 | //go:build consumer 2 | // +build consumer 3 | 4 | package avro 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/pact-foundation/pact-go/v2/consumer" 16 | 17 | "path/filepath" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | var dir, _ = os.Getwd() 23 | 24 | func TestAvroHTTP(t *testing.T) { 25 | mockProvider, err := consumer.NewV4Pact(consumer.MockHTTPProviderConfig{ 26 | Consumer: "AvroConsumer", 27 | Provider: "AvroProvider", 28 | PactDir: filepath.ToSlash(fmt.Sprintf("%s/../pacts", dir)), 29 | }) 30 | assert.NoError(t, err) 31 | 32 | dir, _ := os.Getwd() 33 | path := fmt.Sprintf("%s/user.avsc", strings.ReplaceAll(dir, "\\", "/")) 34 | 35 | avroResponse := `{ 36 | "pact:avro": "` + path + `", 37 | "pact:record-name": "User", 38 | "pact:content-type": "avro/binary", 39 | "id": "matching(number, 1)", 40 | "username": "notEmpty('matt')" 41 | }` 42 | 43 | // Set up our expected interactions. 44 | err = mockProvider. 45 | AddInteraction(). 46 | UponReceiving("A request to do get some Avro stuff"). 47 | UsingPlugin(consumer.PluginConfig{ 48 | Plugin: "avro", 49 | Version: "0.0.6", 50 | }). 51 | WithRequest("GET", "/avro"). 52 | WillRespondWith(200, func(res *consumer.V4InteractionWithPluginResponseBuilder) { 53 | res.PluginContents("avro/binary", avroResponse) 54 | }). 55 | ExecuteTest(t, func(msc consumer.MockServerConfig) error { 56 | resp, err := callServiceHTTP(msc) 57 | 58 | assert.Equal(t, int64(1), resp.ID) 59 | assert.Equal(t, "matt", resp.Username) // ??????! 60 | 61 | return err 62 | }) 63 | assert.NoError(t, err) 64 | 65 | } 66 | 67 | func callServiceHTTP(msc consumer.MockServerConfig) (*User, error) { 68 | client := &http.Client{} 69 | req := &http.Request{ 70 | Method: "GET", 71 | URL: &url.URL{ 72 | Host: fmt.Sprintf("%s:%d", msc.Host, msc.Port), 73 | Scheme: "http", 74 | Path: "/avro", 75 | }, 76 | Header: make(http.Header), 77 | } 78 | 79 | req.Header.Set("Content-Type", "avro/binary;record=User") 80 | 81 | res, err := client.Do(req) 82 | 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | bytes, err := io.ReadAll(res.Body) 88 | 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | codec := getCodec() 94 | native, _, err := codec.NativeFromBinary(bytes) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | user := &User{ 100 | ID: native.(map[string]interface{})["id"].(int64), 101 | Username: native.(map[string]interface{})["username"].(string), 102 | } 103 | 104 | return user, err 105 | } 106 | -------------------------------------------------------------------------------- /examples/avro/avro_provider_test.go: -------------------------------------------------------------------------------- 1 | //go:build provider 2 | // +build provider 3 | 4 | package avro 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/http" 10 | 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | 15 | "github.com/pact-foundation/pact-go/v2/provider" 16 | "github.com/pact-foundation/pact-go/v2/utils" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | var dir, _ = os.Getwd() 21 | var pactDir = fmt.Sprintf("%s/../pacts", dir) 22 | 23 | func TestAvroHTTPProvider(t *testing.T) { 24 | httpPort, _ := utils.GetFreePort() 25 | 26 | // Start provider API in the background 27 | go startHTTPProvider(httpPort) 28 | 29 | verifier := provider.NewVerifier() 30 | 31 | // Verify the Provider with local Pact Files 32 | err := verifier.VerifyProvider(t, provider.VerifyRequest{ 33 | ProviderBaseURL: fmt.Sprintf("http://127.0.0.1:%d", httpPort), 34 | Provider: "AvroProvider", 35 | PactFiles: []string{ 36 | filepath.ToSlash(fmt.Sprintf("%s/AvroConsumer-AvroProvider.json", pactDir)), 37 | }, 38 | }) 39 | 40 | assert.NoError(t, err) 41 | 42 | } 43 | 44 | func startHTTPProvider(port int) { 45 | mux := http.NewServeMux() 46 | 47 | mux.HandleFunc("/avro", func(w http.ResponseWriter, req *http.Request) { 48 | w.Header().Add("Content-Type", "avro/binary;record=User") 49 | 50 | user := &User{ 51 | ID: 1, 52 | Username: "matt", 53 | // Username: "sally", // matching rules not supported? 54 | } 55 | 56 | codec := getCodec() 57 | binary, err := codec.BinaryFromNative(nil, map[string]interface{}{ 58 | "id": user.ID, 59 | "username": user.Username, 60 | }) 61 | if err != nil { 62 | log.Println("ERROR: ", err) 63 | w.WriteHeader(500) 64 | } else { 65 | fmt.Fprintf(w, string(binary)) 66 | w.WriteHeader(200) 67 | } 68 | }) 69 | 70 | log.Printf("started HTTP server on port: %d\n", port) 71 | log.Fatal(http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), mux)) 72 | } 73 | -------------------------------------------------------------------------------- /examples/avro/codec.go: -------------------------------------------------------------------------------- 1 | package avro 2 | 3 | import ( 4 | 5 | "os" 6 | "github.com/linkedin/goavro/v2" 7 | ) 8 | 9 | func getCodec() *goavro.Codec { 10 | schema, err := os.ReadFile("user.avsc") 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | codec, err := goavro.NewCodec(string(schema)) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | return codec 21 | } 22 | -------------------------------------------------------------------------------- /examples/avro/user.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "User", 4 | "namespace": "io.pact", 5 | "fields" : [ 6 | {"name": "id", "type": "long"}, 7 | {"name": "username", "type": "string"} 8 | ] 9 | } -------------------------------------------------------------------------------- /examples/avro/user.go: -------------------------------------------------------------------------------- 1 | package avro 2 | 3 | type User struct { 4 | ID int64 `json:"id"` 5 | Username string `json:"username"` 6 | } 7 | -------------------------------------------------------------------------------- /examples/basic_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/pact-foundation/pact-go/v2/consumer" 10 | "github.com/pact-foundation/pact-go/v2/matchers" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type S = matchers.S 15 | 16 | func TestProductAPIClient(t *testing.T) { 17 | // Specify the two applications in the integration we are testing 18 | // NOTE: this can usually be extracted out of the individual test for re-use) 19 | mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ 20 | Consumer: "PactGoProductAPIConsumer", 21 | Provider: "PactGoProductAPI", 22 | }) 23 | assert.NoError(t, err) 24 | 25 | // Arrange: Setup our expected interactions 26 | err = mockProvider. 27 | AddInteraction(). 28 | Given("A product with ID 10 exists"). 29 | UponReceiving("A request for Product 10"). 30 | WithRequest("GET", "/products/10"). 31 | WillRespondWith(200, func(b *consumer.V2ResponseBuilder) { 32 | b.BodyMatch(&Product{}) 33 | }). 34 | ExecuteTest(t, func(config consumer.MockServerConfig) error { 35 | // Act: test our API client behaves correctly 36 | // Initialise the API client and point it at the Pact mock server 37 | client := newClient(config.Host, config.Port) 38 | 39 | // Execute the API client 40 | product, err := client.GetProduct("10") 41 | 42 | // Assert: check the result 43 | assert.NoError(t, err) 44 | assert.Equal(t, 10, product.ID) 45 | 46 | return err 47 | }) 48 | 49 | assert.NoError(t, err) 50 | } 51 | 52 | // Product domain model 53 | type Product struct { 54 | ID int `json:"id" pact:"example=10"` 55 | Name string `json:"name" pact:"example=Billy"` 56 | Price string `json:"price" pact:"example=23.33"` 57 | } 58 | 59 | // Product API Client to test 60 | type productAPIClient struct { 61 | port int 62 | host string 63 | } 64 | 65 | func newClient(host string, port int) *productAPIClient { 66 | return &productAPIClient{ 67 | host: host, 68 | port: port, 69 | } 70 | } 71 | 72 | func (u *productAPIClient) GetProduct(id string) (*Product, error) { 73 | resp, err := http.Get(fmt.Sprintf("http://%s:%d:%s%s", u.host, u.port, "/products/", id)) 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | product := new(Product) 80 | err = json.NewDecoder(resp.Body).Decode(product) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return product, err 86 | } 87 | -------------------------------------------------------------------------------- /examples/consumer_v2_test.go: -------------------------------------------------------------------------------- 1 | //go:build consumer 2 | // +build consumer 3 | 4 | // Package main contains a runnable Consumer Pact test example. 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/pact-foundation/pact-go/v2/consumer" 16 | "github.com/pact-foundation/pact-go/v2/log" 17 | "github.com/pact-foundation/pact-go/v2/matchers" 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | var Like = matchers.Like 22 | var EachLike = matchers.EachLike 23 | var Term = matchers.Term 24 | var Regex = matchers.Regex 25 | var HexValue = matchers.HexValue 26 | var Identifier = matchers.Identifier 27 | var IPAddress = matchers.IPAddress 28 | var IPv6Address = matchers.IPv6Address 29 | var Timestamp = matchers.Timestamp 30 | var Date = matchers.Date 31 | var Time = matchers.Time 32 | var UUID = matchers.UUID 33 | var ArrayMinLike = matchers.ArrayMinLike 34 | 35 | type Map = matchers.MapMatcher 36 | 37 | func TestConsumerV2(t *testing.T) { 38 | log.SetLogLevel("INFO") 39 | 40 | mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ 41 | Consumer: "PactGoV2Consumer", 42 | Provider: "V2Provider", 43 | Host: "127.0.0.1", 44 | TLS: true, 45 | }) 46 | 47 | assert.NoError(t, err) 48 | 49 | // Set up our expected interactions. 50 | mockProvider. 51 | AddInteraction(). 52 | Given("User foo exists"). 53 | UponReceiving("A request to do a foo"). 54 | WithRequestPathMatcher("POST", Regex("/foobar", `\/foo.*`), func(b *consumer.V2RequestBuilder) { 55 | b. 56 | Header("Content-Type", S("application/json")). 57 | Header("Authorization", Like("Bearer 1234")). 58 | Query("baz", Regex("bar", "[a-z]+"), Regex("bat", "[a-z]+"), Regex("baz", "[a-z]+")). 59 | JSONBody(Map{ 60 | "id": Like(27), 61 | "name": Like("billy"), 62 | "datetime": Like("2020-01-01'T'08:00:45"), 63 | "lastName": Like("billy"), 64 | // "equality": Equality("a thing"), // Add this in and watch me panic 65 | }) 66 | }). 67 | WillRespondWith(200, func(b *consumer.V2ResponseBuilder) { 68 | b.Header("Content-Type", Regex("application/json", "application\\/json")) 69 | b.JSONBody(Map{ 70 | "datetime": Regex("2020-01-01", "[0-9\\-]+"), 71 | "name": S("Billy"), 72 | "lastName": S("Sampson"), 73 | "itemsMin": ArrayMinLike("thereshouldbe3ofthese", 3), 74 | // "equality": Equality("a thing"), // Add this in and watch me panic 75 | }) 76 | }). 77 | ExecuteTest(t, test) 78 | assert.NoError(t, err) 79 | } 80 | 81 | func TestConsumerV2_Match(t *testing.T) { 82 | log.SetLogLevel("INFO") 83 | 84 | mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ 85 | Consumer: "PactGoV2ConsumerMatch", 86 | Provider: "V2ProviderMatch", 87 | Host: "127.0.0.1", 88 | TLS: true, 89 | }) 90 | 91 | assert.NoError(t, err) 92 | 93 | // Set up our expected interactions. 94 | mockProvider. 95 | AddInteraction(). 96 | Given("User foo exists"). 97 | UponReceiving("A request to do a foo"). 98 | WithRequest("POST", "/foobar", func(b *consumer.V2RequestBuilder) { 99 | b.Header("Content-Type", S("application/json")) 100 | b.Header("Authorization", Like("Bearer 1234")) 101 | b.Query("baz", Regex("bar", "[a-z]+"), Regex("bat", "[a-z]+"), Regex("baz", "[a-z]+")) 102 | b.BodyMatch(&User{}) 103 | 104 | }). 105 | WillRespondWith(200, func(b *consumer.V2ResponseBuilder) { 106 | b.Header("Content-Type", Regex("application/json", "application\\/json")) 107 | b.BodyMatch(&User{}) 108 | }). 109 | ExecuteTest(t, test) 110 | assert.NoError(t, err) 111 | } 112 | 113 | func TestConsumerV2AllInOne(t *testing.T) { 114 | log.SetLogLevel("INFO") 115 | 116 | mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{ 117 | Consumer: "PactGoV2ConsumerAllInOne", 118 | Provider: "V2Provider", 119 | Host: "127.0.0.1", 120 | TLS: true, 121 | }) 122 | 123 | assert.NoError(t, err) 124 | 125 | // Set up our expected interactions. 126 | err = mockProvider. 127 | AddInteraction(). 128 | Given("User foo exists"). 129 | UponReceiving("A request to do a foo"). 130 | WithCompleteRequest(consumer.Request{ 131 | Method: "POST", 132 | Path: Regex("/foobar", `\/foo.*`), 133 | Query: Map{ 134 | "baz": Regex("bat", "[a-zA-Z]+"), 135 | }, 136 | Headers: commonHeaders, 137 | Body: Map{ 138 | "id": Like(27), 139 | "name": Like("billy"), 140 | "datetime": Like("2020-01-01'T'08:00:45"), 141 | "lastName": Like("billy"), 142 | // "equality": Equality("a thing"), // Add this in and watch me panic 143 | }, 144 | }). 145 | WithCompleteResponse(consumer.Response{ 146 | Headers: commonHeaders, 147 | Status: 200, 148 | Body: Map{ 149 | "datetime": Regex("2020-01-01", "[0-9\\-]+"), 150 | "name": S("Billy"), 151 | "lastName": S("Sampson"), 152 | "itemsMin": ArrayMinLike("thereshouldbe3ofthese", 3), 153 | // "equality": Equality("a thing"), // Add this in and watch me panic 154 | }, 155 | }). 156 | ExecuteTest(t, legacyTest) 157 | assert.NoError(t, err) 158 | } 159 | 160 | type User struct { 161 | ID int `json:"id" pact:"example=27"` 162 | Name string `json:"name" pact:"example=Billy"` 163 | LastName string `json:"lastName" pact:"example=Sampson"` 164 | Date string `json:"datetime" pact:"example=2020-01-01'T'08:00:45,format=yyyy-MM-dd'T'HH:mm:ss,generator=datetime"` 165 | } 166 | 167 | // Pass in test case 168 | 169 | var test = func() func(config consumer.MockServerConfig) error { 170 | return rawTest("baz=bat&baz=foo&baz=something") 171 | }() 172 | 173 | var rawTest = func(query string) func(config consumer.MockServerConfig) error { 174 | 175 | return func(config consumer.MockServerConfig) error { 176 | 177 | config.TLSConfig.InsecureSkipVerify = true 178 | client := &http.Client{ 179 | Transport: &http.Transport{ 180 | TLSClientConfig: config.TLSConfig, 181 | }, 182 | } 183 | req := &http.Request{ 184 | Method: "POST", 185 | URL: &url.URL{ 186 | Host: fmt.Sprintf("%s:%d", "localhost", config.Port), 187 | Scheme: "https", 188 | Path: "/foobar", 189 | RawQuery: query, 190 | }, 191 | Body: io.NopCloser(strings.NewReader(`{"id": 27, "name":"billy", "lastName":"sampson", "datetime":"2021-01-01T08:00:45"}`)), 192 | Header: make(http.Header), 193 | } 194 | 195 | // NOTE: by default, request bodies are expected to be sent with a Content-Type 196 | // of application/json. If you don't explicitly set the content-type, you 197 | // will get a mismatch during Verification. 198 | req.Header.Set("Content-Type", "application/json") 199 | req.Header.Set("Authorization", "Bearer 1234") 200 | 201 | _, err := client.Do(req) 202 | 203 | return err 204 | } 205 | } 206 | 207 | var commonHeaders = Map{ 208 | "Content-Type": S("application/json"), 209 | } 210 | 211 | var legacyTest = func() func(config consumer.MockServerConfig) error { 212 | return rawTest("baz=bat") 213 | }() 214 | -------------------------------------------------------------------------------- /examples/consumer_v3_test.go: -------------------------------------------------------------------------------- 1 | //go:build consumer 2 | // +build consumer 3 | 4 | // Package main contains a runnable Consumer Pact test example. 5 | package main 6 | 7 | import ( 8 | "errors" 9 | "testing" 10 | 11 | "github.com/pact-foundation/pact-go/v2/consumer" 12 | "github.com/pact-foundation/pact-go/v2/log" 13 | "github.com/pact-foundation/pact-go/v2/matchers" 14 | message "github.com/pact-foundation/pact-go/v2/message/v3" 15 | "github.com/pact-foundation/pact-go/v2/models" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | var Decimal = matchers.Decimal 20 | var Integer = matchers.Integer 21 | var Equality = matchers.Equality 22 | var Includes = matchers.Includes 23 | var FromProviderState = matchers.FromProviderState 24 | var EachKeyLike = matchers.EachKeyLike 25 | var ArrayContaining = matchers.ArrayContaining 26 | var ArrayMinMaxLike = matchers.ArrayMinMaxLike 27 | var ArrayMaxLike = matchers.ArrayMaxLike 28 | var DateGenerated = matchers.DateGenerated 29 | var TimeGenerated = matchers.TimeGenerated 30 | var DateTimeGenerated = matchers.DateTimeGenerated 31 | 32 | func TestConsumerV3(t *testing.T) { 33 | log.SetLogLevel("INFO") 34 | 35 | mockProvider, err := consumer.NewV3Pact(consumer.MockHTTPProviderConfig{ 36 | Consumer: "PactGoV3Consumer", 37 | Provider: "V3Provider", 38 | Host: "127.0.0.1", 39 | TLS: true, 40 | }) 41 | assert.NoError(t, err) 42 | 43 | // Set up our expected interactions. 44 | err = mockProvider. 45 | AddInteraction(). 46 | Given("state 1"). 47 | GivenWithParameter(models.ProviderState{ 48 | Name: "User foo exists", 49 | Parameters: map[string]interface{}{ 50 | "id": "foo", 51 | }, 52 | }). 53 | UponReceiving("A request to do a foo"). 54 | WithRequest("POST", "/foobar", func(b *consumer.V3RequestBuilder) { 55 | b. 56 | Header("Content-Type", S("application/json")). 57 | Header("Authorization", Like("Bearer 1234")). 58 | Query("baz", Regex("bar", "[a-z]+"), Regex("bat", "[a-z]+"), Regex("baz", "[a-z]+")). 59 | JSONBody(Map{ 60 | "id": Like(27), 61 | "name": FromProviderState("${name}", "billy"), 62 | "lastName": Like("billy"), 63 | "datetime": DateTimeGenerated("2020-01-01T08:00:45", "yyyy-MM-dd'T'HH:mm:ss"), 64 | }) 65 | 66 | }). 67 | WillRespondWith(200, func(b *consumer.V3ResponseBuilder) { 68 | b. 69 | Header("Content-Type", S("application/json")). 70 | JSONBody(Map{ 71 | "datetime": Regex("2020-01-01", "[0-9\\-]+"), 72 | "name": S("Billy"), 73 | "lastName": S("Sampson"), 74 | "superstring": Includes("foo"), 75 | "id": Integer(12), 76 | "accountBalance": Decimal(123.76), 77 | "itemsMinMax": ArrayMinMaxLike(27, 3, 5), 78 | "itemsMin": ArrayMinLike("thereshouldbe3ofthese", 3), 79 | "equality": Equality("a thing"), 80 | "arrayContaining": ArrayContaining([]interface{}{ 81 | Like("string"), 82 | Integer(1), 83 | Map{ 84 | "foo": Like("bar"), 85 | }, 86 | }), 87 | }) 88 | }). 89 | ExecuteTest(t, test) 90 | assert.NoError(t, err) 91 | } 92 | func TestMessagePact(t *testing.T) { 93 | log.SetLogLevel("INFO") 94 | 95 | provider, err := message.NewMessagePact(message.Config{ 96 | Consumer: "PactGoV3MessageConsumer", 97 | Provider: "V3MessageProvider", // must be different to the HTTP one, can't mix both interaction styles 98 | }) 99 | assert.NoError(t, err) 100 | 101 | err = provider.AddMessage(). 102 | GivenWithParameter(models.ProviderState{ 103 | Name: "User with id 127 exists", 104 | Parameters: map[string]interface{}{ 105 | "id": 127, 106 | }, 107 | }). 108 | ExpectsToReceive("a user event"). 109 | WithMetadata(map[string]string{ 110 | "Content-Type": "application/json", 111 | }). 112 | WithJSONContent(Map{ 113 | "datetime": Regex("2020-01-01", "[0-9\\-]+"), 114 | "name": S("Billy"), 115 | "lastName": S("Sampson"), 116 | "id": Integer(12), 117 | }). 118 | AsType(&User{}). 119 | ConsumedBy(userHandlerWrapper). 120 | Verify(t) 121 | 122 | assert.NoError(t, err) 123 | } 124 | 125 | // Message Pact - wrapped handler extracts the message 126 | var userHandlerWrapper = func(m message.MessageContents) error { 127 | return userHandler(*m.Content.(*User)) 128 | } 129 | 130 | // Message Pact - actual handler 131 | var userHandler = func(u User) error { 132 | if u.ID == 0 { 133 | return errors.New("invalid object supplied, missing fields (id)") 134 | } 135 | 136 | // ... actually consume the message 137 | 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /examples/consumer_v4_test.go: -------------------------------------------------------------------------------- 1 | //go:build consumer 2 | // +build consumer 3 | 4 | // Package main contains a runnable Consumer Pact test example. 5 | package main 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/pact-foundation/pact-go/v2/consumer" 11 | "github.com/pact-foundation/pact-go/v2/log" 12 | "github.com/pact-foundation/pact-go/v2/models" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestConsumerV4(t *testing.T) { 17 | log.SetLogLevel("INFO") 18 | 19 | mockProvider, err := consumer.NewV4Pact(consumer.MockHTTPProviderConfig{ 20 | Consumer: "PactGoV4Consumer", 21 | Provider: "V4Provider", 22 | Host: "127.0.0.1", 23 | TLS: true, 24 | }) 25 | assert.NoError(t, err) 26 | 27 | // Set up our expected interactions. 28 | err = mockProvider. 29 | AddInteraction(). 30 | Given("state 1"). 31 | GivenWithParameter(models.ProviderState{ 32 | Name: "User foo exists", 33 | Parameters: map[string]interface{}{ 34 | "id": "foo", 35 | }, 36 | }). 37 | UponReceiving("A request to do a foo"). 38 | WithRequest("POST", "/foobar", func(b *consumer.V4RequestBuilder) { 39 | b. 40 | Header("Content-Type", S("application/json")). 41 | Header("Authorization", Like("Bearer 1234")). 42 | Query("baz", Regex("bar", "[a-z]+"), Regex("bat", "[a-z]+"), Regex("baz", "[a-z]+")). 43 | JSONBody(Map{ 44 | "id": Like(27), 45 | "name": FromProviderState("${name}", "billy"), 46 | "lastName": Like("billy"), 47 | "datetime": DateTimeGenerated("2020-01-01T08:00:45", "yyyy-MM-dd'T'HH:mm:ss"), 48 | }) 49 | 50 | }). 51 | WillRespondWith(200, func(b *consumer.V4ResponseBuilder) { 52 | b. 53 | Header("Content-Type", S("application/json")). 54 | JSONBody(Map{ 55 | "datetime": Regex("2020-01-01", "[0-9\\-]+"), 56 | "name": S("Billy"), 57 | "lastName": S("Sampson"), 58 | "superstring": Includes("foo"), 59 | "id": Integer(12), 60 | "accountBalance": Decimal(123.76), 61 | "itemsMinMax": ArrayMinMaxLike(27, 3, 5), 62 | "itemsMin": ArrayMinLike("thereshouldbe3ofthese", 3), 63 | "equality": Equality("a thing"), 64 | "arrayContaining": ArrayContaining([]interface{}{ 65 | Like("string"), 66 | Integer(1), 67 | Map{ 68 | "foo": Like("bar"), 69 | }, 70 | }), 71 | }) 72 | }). 73 | ExecuteTest(t, test) 74 | assert.NoError(t, err) 75 | } 76 | -------------------------------------------------------------------------------- /examples/grpc/common.go: -------------------------------------------------------------------------------- 1 | //go:build consumer || provider 2 | 3 | package grpc 4 | 5 | import "os" 6 | 7 | var dir, _ = os.Getwd() 8 | -------------------------------------------------------------------------------- /examples/grpc/grpc_provider_test.go: -------------------------------------------------------------------------------- 1 | //go:build provider 2 | // +build provider 3 | 4 | package grpc 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "net" 11 | "path/filepath" 12 | "testing" 13 | 14 | pb "github.com/pact-foundation/pact-go/v2/examples/grpc/routeguide" 15 | "github.com/pact-foundation/pact-go/v2/examples/grpc/routeguide/server" 16 | l "github.com/pact-foundation/pact-go/v2/log" 17 | "github.com/pact-foundation/pact-go/v2/provider" 18 | "github.com/stretchr/testify/assert" 19 | "google.golang.org/grpc" 20 | ) 21 | 22 | func TestGrpcProvider(t *testing.T) { 23 | go startProvider() 24 | l.SetLogLevel("INFO") 25 | 26 | verifier := provider.NewVerifier() 27 | 28 | err := verifier.VerifyProvider(t, provider.VerifyRequest{ 29 | ProviderBaseURL: "http://localhost:8222", 30 | Transports: []provider.Transport{ 31 | { 32 | Protocol: "grpc", 33 | Port: 8222, 34 | }, 35 | }, 36 | Provider: "grpcprovider", 37 | PactFiles: []string{ 38 | filepath.ToSlash(fmt.Sprintf("%s/../pacts/grpcconsumer-grpcprovider.json", dir)), 39 | }, 40 | }) 41 | 42 | assert.NoError(t, err) 43 | } 44 | 45 | func startProvider() { 46 | lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", 8222)) 47 | if err != nil { 48 | log.Fatalf("failed to listen: %v", err) 49 | } 50 | var opts []grpc.ServerOption 51 | grpcServer := grpc.NewServer(opts...) 52 | pb.RegisterRouteGuideServer(grpcServer, server.NewServer()) 53 | grpcServer.Serve(lis) 54 | } 55 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/data.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 gRPC authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | // Package data provides convenience routines to access files in the data 19 | // directory. 20 | package data 21 | 22 | import ( 23 | "path/filepath" 24 | "runtime" 25 | ) 26 | 27 | // basepath is the root directory of this package. 28 | var basepath string 29 | 30 | func init() { 31 | _, currentFile, _, _ := runtime.Caller(0) 32 | basepath = filepath.Dir(currentFile) 33 | } 34 | 35 | // Path returns the absolute path the given relative file or directory path, 36 | // relative to the google.golang.org/grpc/examples/data directory in the 37 | // user's GOPATH. If rel is already absolute, it is returned unmodified. 38 | func Path(rel string) string { 39 | if filepath.IsAbs(rel) { 40 | return rel 41 | } 42 | 43 | return filepath.Join(basepath, rel) 44 | } 45 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/README.md: -------------------------------------------------------------------------------- 1 | This directory contains x509 certificates and associated private keys used in 2 | examples. 3 | 4 | How were these test certs/keys generated ? 5 | ------------------------------------------ 6 | Run `./create.sh` 7 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/ca_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | qQCK9iiN08MAFNia/0/Bj4D7XKurNRY= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/client_ca_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/client_ca_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | D6kBMuZ4gkKW24IYzXzOni0/BOnpOfM= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/client_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | MoO9n00= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/client_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create the server CA certs. 4 | openssl req -x509 \ 5 | -newkey rsa:4096 \ 6 | -nodes \ 7 | -days 3650 \ 8 | -keyout ca_key.pem \ 9 | -out ca_cert.pem \ 10 | -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-server_ca/ \ 11 | -config ./openssl.cnf \ 12 | -extensions test_ca \ 13 | -sha256 14 | 15 | # Create the client CA certs. 16 | openssl req -x509 \ 17 | -newkey rsa:4096 \ 18 | -nodes \ 19 | -days 3650 \ 20 | -keyout client_ca_key.pem \ 21 | -out client_ca_cert.pem \ 22 | -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client_ca/ \ 23 | -config ./openssl.cnf \ 24 | -extensions test_ca \ 25 | -sha256 26 | 27 | # Generate a server cert. 28 | openssl genrsa -out server_key.pem 4096 29 | openssl req -new \ 30 | -key server_key.pem \ 31 | -days 3650 \ 32 | -out server_csr.pem \ 33 | -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-server1/ \ 34 | -config ./openssl.cnf \ 35 | -reqexts test_server 36 | openssl x509 -req \ 37 | -in server_csr.pem \ 38 | -CAkey ca_key.pem \ 39 | -CA ca_cert.pem \ 40 | -days 3650 \ 41 | -set_serial 1000 \ 42 | -out server_cert.pem \ 43 | -extfile ./openssl.cnf \ 44 | -extensions test_server \ 45 | -sha256 46 | openssl verify -verbose -CAfile ca_cert.pem server_cert.pem 47 | 48 | # Generate a client cert. 49 | openssl genrsa -out client_key.pem 4096 50 | openssl req -new \ 51 | -key client_key.pem \ 52 | -days 3650 \ 53 | -out client_csr.pem \ 54 | -subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client1/ \ 55 | -config ./openssl.cnf \ 56 | -reqexts test_client 57 | openssl x509 -req \ 58 | -in client_csr.pem \ 59 | -CAkey client_ca_key.pem \ 60 | -CA client_ca_cert.pem \ 61 | -days 3650 \ 62 | -set_serial 1000 \ 63 | -out client_cert.pem \ 64 | -extfile ./openssl.cnf \ 65 | -extensions test_client \ 66 | -sha256 67 | openssl verify -verbose -CAfile client_ca_cert.pem client_cert.pem 68 | 69 | rm *_csr.pem 70 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/openssl.cnf: -------------------------------------------------------------------------------- 1 | [req] 2 | distinguished_name = req_distinguished_name 3 | attributes = req_attributes 4 | 5 | [req_distinguished_name] 6 | 7 | [req_attributes] 8 | 9 | [test_ca] 10 | basicConstraints = critical,CA:TRUE 11 | subjectKeyIdentifier = hash 12 | authorityKeyIdentifier = keyid:always,issuer:always 13 | keyUsage = critical,keyCertSign 14 | 15 | [test_server] 16 | basicConstraints = critical,CA:FALSE 17 | subjectKeyIdentifier = hash 18 | keyUsage = critical,digitalSignature,keyEncipherment,keyAgreement 19 | subjectAltName = @server_alt_names 20 | 21 | [server_alt_names] 22 | DNS.1 = *.test.example.com 23 | 24 | [test_client] 25 | basicConstraints = critical,CA:FALSE 26 | subjectKeyIdentifier = hash 27 | keyUsage = critical,nonRepudiation,digitalSignature,keyEncipherment 28 | extendedKeyUsage = critical,clientAuth 29 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/server_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | yaBwLiXfcvZrZOwy 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/data/x509/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /examples/grpc/routeguide/route_guide.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | option go_package = "google.golang.org/grpc/examples/route_guide/routeguide"; 18 | option java_multiple_files = true; 19 | option java_package = "io.grpc.examples.routeguide"; 20 | option java_outer_classname = "RouteGuideProto"; 21 | 22 | package routeguide; 23 | 24 | // Interface exported by the server. 25 | service RouteGuide { 26 | // A simple RPC. 27 | // 28 | // Obtains the feature at a given position. 29 | // 30 | // A feature with an empty name is returned if there's no feature at the given 31 | // position. 32 | rpc GetFeature(Point) returns (Feature) {} 33 | 34 | // Save the feature. 35 | rpc SaveFeature(Feature) returns (Feature) {} 36 | 37 | // A server-to-client streaming RPC. 38 | // 39 | // Obtains the Features available within the given Rectangle. Results are 40 | // streamed rather than returned at once (e.g. in a response message with a 41 | // repeated field), as the rectangle may cover a large area and contain a 42 | // huge number of features. 43 | rpc ListFeatures(Rectangle) returns (stream Feature) {} 44 | 45 | // A client-to-server streaming RPC. 46 | // 47 | // Accepts a stream of Points on a route being traversed, returning a 48 | // RouteSummary when traversal is completed. 49 | rpc RecordRoute(stream Point) returns (RouteSummary) {} 50 | 51 | // A Bidirectional streaming RPC. 52 | // 53 | // Accepts a stream of RouteNotes sent while a route is being traversed, 54 | // while receiving other RouteNotes (e.g. from other users). 55 | rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 56 | } 57 | 58 | // Points are represented as latitude-longitude pairs in the E7 representation 59 | // (degrees multiplied by 10**7 and rounded to the nearest integer). 60 | // Latitudes should be in the range +/- 90 degrees and longitude should be in 61 | // the range +/- 180 degrees (inclusive). 62 | message Point { 63 | int32 latitude = 1; 64 | int32 longitude = 2; 65 | } 66 | 67 | // A latitude-longitude rectangle, represented as two diagonally opposite 68 | // points "lo" and "hi". 69 | message Rectangle { 70 | // One corner of the rectangle. 71 | Point lo = 1; 72 | 73 | // The other corner of the rectangle. 74 | Point hi = 2; 75 | } 76 | 77 | // A feature names something at a given point. 78 | // 79 | // If a feature could not be named, the name is empty. 80 | message Feature { 81 | // The name of the feature. 82 | string name = 1; 83 | 84 | // The point where the feature is detected. 85 | Point location = 2; 86 | 87 | // A description of the feature. 88 | string description = 3; 89 | } 90 | 91 | // A RouteNote is a message sent while at a given point. 92 | message RouteNote { 93 | // The location from which the message is sent. 94 | Point location = 1; 95 | 96 | // The message to be sent. 97 | string message = 2; 98 | } 99 | 100 | // A RouteSummary is received in response to a RecordRoute rpc. 101 | // 102 | // It contains the number of individual points received, the number of 103 | // detected features, and the total distance covered as the cumulative sum of 104 | // the distance between each point. 105 | message RouteSummary { 106 | // The number of points received. 107 | int32 point_count = 1; 108 | 109 | // The number of known features passed while traversing the route. 110 | int32 feature_count = 2; 111 | 112 | // The distance covered in metres. 113 | int32 distance = 3; 114 | 115 | // The duration of the traversal in seconds. 116 | int32 elapsed_time = 4; 117 | } 118 | -------------------------------------------------------------------------------- /examples/plugin/consumer_plugin_test.go: -------------------------------------------------------------------------------- 1 | //go:build consumer 2 | // +build consumer 3 | 4 | package plugin 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | "strings" 16 | "testing" 17 | 18 | "github.com/pact-foundation/pact-go/v2/consumer" 19 | 20 | // "github.com/pact-foundation/pact-go/v2/matchers" 21 | message "github.com/pact-foundation/pact-go/v2/message/v4" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | var dir, _ = os.Getwd() 26 | 27 | func TestHTTPPlugin(t *testing.T) { 28 | mockProvider, err := consumer.NewV4Pact(consumer.MockHTTPProviderConfig{ 29 | Consumer: "MattConsumer", 30 | Provider: "MattProvider", 31 | PactDir: filepath.ToSlash(fmt.Sprintf("%s/../pacts", dir)), 32 | }) 33 | assert.NoError(t, err) 34 | 35 | // MATT is a protocol, where all message start and end with a MATT 36 | mattRequest := `{"request": {"body": "hello"}}` 37 | mattResponse := `{"response":{"body":"world"}}` 38 | 39 | // Set up our expected interactions. 40 | err = mockProvider. 41 | AddInteraction(). 42 | UponReceiving("A request to do a matt"). 43 | UsingPlugin(consumer.PluginConfig{ 44 | Plugin: "matt", 45 | Version: "0.1.1", 46 | }). 47 | WithRequest("POST", "/matt", func(req *consumer.V4InteractionWithPluginRequestBuilder) { 48 | req.PluginContents("application/matt", mattRequest) 49 | }). 50 | WillRespondWith(200, func(res *consumer.V4InteractionWithPluginResponseBuilder) { 51 | res.PluginContents("application/matt", mattResponse) 52 | }). 53 | ExecuteTest(t, func(msc consumer.MockServerConfig) error { 54 | resp, err := callMattServiceHTTP(msc, "hello") 55 | 56 | assert.Equal(t, "world", resp) 57 | 58 | return err 59 | }) 60 | assert.NoError(t, err) 61 | } 62 | 63 | func TestTCPPlugin(t *testing.T) { 64 | p, _ := message.NewSynchronousPact(message.Config{ 65 | Consumer: "matttcpconsumer", 66 | Provider: "matttcpprovider", 67 | PactDir: filepath.ToSlash(fmt.Sprintf("%s/../pacts", dir)), 68 | }) 69 | 70 | // MATT is a protocol, where all message start and end with a MATT 71 | mattMessage := `{"request": {"body": "hellotcp"}, "response":{"body":"tcpworld"}}` 72 | 73 | err := p.AddSynchronousMessage("Matt message"). 74 | Given("the world exists"). 75 | UsingPlugin(message.PluginConfig{ 76 | Plugin: "matt", 77 | Version: "0.1.1", 78 | }). 79 | WithContents(mattMessage, "application/matt"). 80 | StartTransport("matt", "127.0.0.1", nil). // For plugin tests, we can't assume if a transport is needed, so this is optional 81 | ExecuteTest(t, func(transport message.TransportConfig, m message.SynchronousMessage) error { 82 | fmt.Println("matt TCP transport running on", transport) 83 | 84 | str, err := callMattServiceTCP(transport, "hellotcp") 85 | 86 | assert.Equal(t, "tcpworld", str) 87 | return err 88 | }) 89 | 90 | assert.NoError(t, err) 91 | } 92 | 93 | func callMattServiceHTTP(msc consumer.MockServerConfig, message string) (string, error) { 94 | client := &http.Client{} 95 | req := &http.Request{ 96 | Method: "POST", 97 | URL: &url.URL{ 98 | Host: fmt.Sprintf("%s:%d", msc.Host, msc.Port), 99 | Scheme: "http", 100 | Path: "/matt", 101 | }, 102 | Body: io.NopCloser(strings.NewReader(generateMattMessage(message))), 103 | Header: make(http.Header), 104 | } 105 | 106 | req.Header.Set("Content-Type", "application/matt") 107 | 108 | res, err := client.Do(req) 109 | 110 | if err != nil { 111 | return "", err 112 | } 113 | 114 | bytes, err := io.ReadAll(res.Body) 115 | 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | return parseMattMessage(string(bytes)), err 121 | } 122 | 123 | func callMattServiceTCP(transport message.TransportConfig, message string) (string, error) { 124 | conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", transport.Address, transport.Port)) 125 | if err != nil { 126 | return "", err 127 | } 128 | 129 | conn.Write([]byte(generateMattMessage(message))) 130 | 131 | str, err := bufio.NewReader(conn).ReadString('\n') 132 | 133 | if err != nil { 134 | return "", err 135 | } 136 | 137 | return parseMattMessage(str), nil 138 | } 139 | 140 | func generateMattMessage(message string) string { 141 | return fmt.Sprintf("MATT%sMATT\n", message) 142 | } 143 | 144 | func parseMattMessage(message string) string { 145 | return strings.TrimSpace(strings.ReplaceAll(message, "MATT", "")) 146 | } 147 | -------------------------------------------------------------------------------- /examples/plugin/provider_plugin_test.go: -------------------------------------------------------------------------------- 1 | //go:build provider 2 | // +build provider 3 | 4 | package plugin 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | "testing" 16 | 17 | // "github.com/pact-foundation/pact-go/v2/log" 18 | "log" 19 | 20 | "github.com/pact-foundation/pact-go/v2/provider" 21 | "github.com/pact-foundation/pact-go/v2/utils" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | var dir, _ = os.Getwd() 26 | var pactDir = fmt.Sprintf("%s/../pacts", dir) 27 | 28 | func TestPluginProvider(t *testing.T) { 29 | t.Skip() 30 | httpPort, _ := utils.GetFreePort() 31 | tcpPort, _ := utils.GetFreePort() 32 | 33 | // Start provider API in the background 34 | go startHTTPProvider(httpPort) 35 | go startTCPServer(tcpPort) 36 | 37 | verifier := provider.NewVerifier() 38 | 39 | // Verify the Provider with local Pact Files 40 | err := verifier.VerifyProvider(t, provider.VerifyRequest{ 41 | ProviderBaseURL: fmt.Sprintf("http://127.0.0.1:%d", httpPort), 42 | // Provider: "provider", 43 | PactFiles: []string{ 44 | filepath.ToSlash(fmt.Sprintf("%s/MattConsumer-MattProvider.json", pactDir)), 45 | filepath.ToSlash(fmt.Sprintf("%s/matttcpconsumer-matttcpprovider.json", pactDir)), 46 | }, 47 | Transports: []provider.Transport{ 48 | provider.Transport{ 49 | Protocol: "matt", 50 | Port: uint16(tcpPort), 51 | Scheme: "tcp", 52 | }, 53 | }, 54 | }) 55 | 56 | assert.NoError(t, err) 57 | } 58 | 59 | func startHTTPProvider(port int) { 60 | mux := http.NewServeMux() 61 | 62 | mux.HandleFunc("/matt", func(w http.ResponseWriter, req *http.Request) { 63 | w.Header().Add("Content-Type", "application/matt") 64 | fmt.Fprintf(w, `MATTworldMATT`) 65 | w.WriteHeader(200) 66 | }) 67 | 68 | log.Printf("started HTTP server on port: %d\n", port) 69 | log.Fatal(http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), mux)) 70 | } 71 | 72 | func startTCPServer(port int) { 73 | log.Println("Starting TCP server on port", port) 74 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 75 | if err != nil { 76 | log.Println("ERROR:", err) 77 | } 78 | 79 | log.Println("TCP server started on port", port) 80 | 81 | for { 82 | conn, err := listener.Accept() 83 | if err != nil { 84 | log.Println("TCP connection error:", err) 85 | continue 86 | } 87 | 88 | log.Println("TCP connection established with:", conn.RemoteAddr()) 89 | 90 | go handleConnection(conn) 91 | } 92 | } 93 | 94 | func handleConnection(conn net.Conn) { 95 | log.Println("Handling TCP connection") 96 | defer conn.Close() 97 | 98 | s := bufio.NewScanner(conn) 99 | 100 | for s.Scan() { 101 | 102 | data := s.Text() 103 | log.Println("Data received from connection", data) 104 | 105 | if data == "" { 106 | continue 107 | } 108 | 109 | handleRequest(data, conn) 110 | } 111 | } 112 | 113 | func handleRequest(req string, conn net.Conn) { 114 | log.Println("TCP Server received request", req, "on connection", conn) 115 | 116 | if !isValidMessage(req) { 117 | log.Println("TCP Server received invalid request, erroring") 118 | conn.Write([]byte("ERROR\n")) 119 | } 120 | log.Println("TCP Server received valid request, responding") 121 | 122 | // var expectedResponse = "badworld" 123 | var expectedResponse = "tcpworld" 124 | conn.Write([]byte(generateMattMessage(expectedResponse))) 125 | conn.Write([]byte("\n")) 126 | } 127 | 128 | func generateMattMessage(message string) string { 129 | return fmt.Sprintf("MATT%sMATT", message) 130 | } 131 | 132 | func parseMattMessage(message string) string { 133 | return strings.ReplaceAll(message, "MATT", "") 134 | } 135 | 136 | func isValidMessage(str string) bool { 137 | matched, err := regexp.MatchString(`^MATT.*MATT$`, str) 138 | if err != nil { 139 | return false 140 | } 141 | 142 | return matched 143 | } 144 | -------------------------------------------------------------------------------- /examples/protobuf-message/protobuf_consumer_test.go: -------------------------------------------------------------------------------- 1 | //go:build consumer 2 | // +build consumer 3 | 4 | package protobuf 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | 13 | message "github.com/pact-foundation/pact-go/v2/message/v4" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var dir, _ = os.Getwd() 18 | 19 | func TestPluginMessageConsumer(t *testing.T) { 20 | p, _ := message.NewAsynchronousPact(message.Config{ 21 | Consumer: "protobufmessageconsumer", 22 | Provider: "protobufmessageprovider", 23 | PactDir: filepath.ToSlash(fmt.Sprintf("%s/../pacts", dir)), 24 | }) 25 | 26 | dir, _ := os.Getwd() 27 | path := fmt.Sprintf("%s/../grpc/routeguide/route_guide.proto", strings.ReplaceAll(dir, "\\", "/")) 28 | 29 | protoMessage := `{ 30 | "pact:proto": "` + path + `", 31 | "pact:message-type": "Feature", 32 | "pact:content-type": "application/protobuf", 33 | 34 | "name": "notEmpty('Big Tree')", 35 | "location": { 36 | "latitude": "matching(number, 180)", 37 | "longitude": "matching(number, 200)" 38 | } 39 | }` 40 | 41 | err := p.AddAsynchronousMessage(). 42 | Given("the world exists"). 43 | ExpectsToReceive("feature message"). 44 | UsingPlugin(message.PluginConfig{ 45 | Plugin: "protobuf", 46 | Version: "0.5.4", 47 | }). 48 | WithContents(protoMessage, "application/protobuf"). 49 | ExecuteTest(t, func(m message.AsynchronousMessage) error { 50 | // TODO: normally would actually read/consume the message 51 | return nil 52 | }) 53 | 54 | assert.NoError(t, err) 55 | } 56 | -------------------------------------------------------------------------------- /examples/protobuf-message/protobuf_provider_test.go: -------------------------------------------------------------------------------- 1 | //go:build provider 2 | // +build provider 3 | 4 | package protobuf 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/golang/protobuf/proto" 13 | "github.com/pact-foundation/pact-go/v2/examples/grpc/routeguide" 14 | pactlog "github.com/pact-foundation/pact-go/v2/log" 15 | "github.com/pact-foundation/pact-go/v2/message" 16 | "github.com/pact-foundation/pact-go/v2/models" 17 | "github.com/pact-foundation/pact-go/v2/provider" 18 | pactversion "github.com/pact-foundation/pact-go/v2/version" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func TestPluginMessageProvider(t *testing.T) { 23 | var dir, _ = os.Getwd() 24 | var pactDir = fmt.Sprintf("%s/../pacts", dir) 25 | 26 | err := pactlog.SetLogLevel("INFO") 27 | assert.NoError(t, err) 28 | 29 | pactversion.CheckVersion() 30 | 31 | verifier := provider.NewVerifier() 32 | 33 | functionMappings := message.Handlers{ 34 | "feature message": func([]models.ProviderState) (message.Body, message.Metadata, error) { 35 | fmt.Println("feature message handler") 36 | feature, _ := proto.Marshal(&routeguide.Feature{ 37 | Name: "fake feature", 38 | Location: &routeguide.Point{ 39 | Latitude: int32(1), 40 | Longitude: int32(1), 41 | }, 42 | }) 43 | return feature, message.Metadata{ 44 | "contentType": "application/protobuf;message=.routeguide.Feature", // <- This is required to ensure the correct type is matched 45 | }, nil 46 | }, 47 | } 48 | 49 | err = verifier.VerifyProvider(t, provider.VerifyRequest{ 50 | PactFiles: []string{ 51 | filepath.ToSlash(fmt.Sprintf("%s/protobufmessageconsumer-protobufmessageprovider.json", pactDir)), 52 | }, 53 | Provider: "protobufmessageprovider", 54 | MessageHandlers: functionMappings, 55 | }) 56 | 57 | assert.NoError(t, err) 58 | } 59 | -------------------------------------------------------------------------------- /examples/types/repository.go: -------------------------------------------------------------------------------- 1 | // package v3 contains types to use across the Consumer/Provider tests. 2 | package types 3 | 4 | // UserRepository is an in-memory user database. 5 | type UserRepository struct { 6 | Users map[string]*User 7 | } 8 | 9 | // ByUsername finds a user by their username. 10 | func (u *UserRepository) ByUsername(username string) (*User, error) { 11 | if user, ok := u.Users[username]; ok { 12 | return user, nil 13 | } 14 | return nil, ErrNotFound 15 | } 16 | 17 | // ByID finds a user by their ID 18 | func (u *UserRepository) ByID(ID int) (*User, error) { 19 | for _, user := range u.Users { 20 | if user.ID == ID { 21 | return user, nil 22 | } 23 | } 24 | return nil, ErrNotFound 25 | } 26 | -------------------------------------------------------------------------------- /examples/types/user_service.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "errors" 4 | 5 | // User is a representation of a User. Dah. 6 | type User struct { 7 | Name string `json:"name" pact:"example=Jean-Marie de La Beaujardière😀😍"` 8 | Username string `json:"username" pact:"example=jmarie"` 9 | Password string `json:"password" pact:"example=password123"` 10 | Type string `json:"type" pact:"example=admin,regex=^(admin|user|guest)$"` 11 | ID int `json:"id" pact:"example=10"` 12 | } 13 | 14 | var ( 15 | // ErrNotFound represents a resource not found (404) 16 | ErrNotFound = errors.New("not found") 17 | 18 | // ErrUnauthorized represents a Forbidden (403) 19 | ErrUnauthorized = errors.New("unauthorized") 20 | 21 | // ErrEmpty is returned when input string is empty 22 | ErrEmpty = errors.New("empty string") 23 | ) 24 | 25 | // LoginRequest is the login request API struct. 26 | type LoginRequest struct { 27 | Username string `json:"username" pact:"example=jmarie"` 28 | Password string `json:"password" pact:"example=issilly"` 29 | } 30 | 31 | // LoginResponse is the login response API struct. 32 | type LoginResponse struct { 33 | User *User `json:"user"` 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pact-foundation/pact-go/v2 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.4 7 | github.com/hashicorp/go-version v1.7.0 8 | github.com/hashicorp/logutils v1.0.0 9 | github.com/linkedin/goavro/v2 v2.13.1 10 | github.com/spf13/afero v1.12.0 11 | github.com/spf13/cobra v1.9.1 12 | github.com/stretchr/testify v1.10.0 13 | google.golang.org/grpc v1.71.0 14 | google.golang.org/protobuf v1.36.5 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/golang/snappy v0.0.4 // indirect 21 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 22 | github.com/kr/pretty v0.3.0 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/rogpeppe/go-internal v1.9.0 // indirect 25 | github.com/spf13/pflag v1.0.6 // indirect 26 | golang.org/x/net v0.34.0 // indirect 27 | golang.org/x/sys v0.29.0 // indirect 28 | golang.org/x/text v0.21.0 // indirect 29 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 30 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 7 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 8 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 9 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 10 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 11 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 12 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 13 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 14 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 15 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 16 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 17 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 18 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 19 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 20 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 21 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 22 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 23 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 24 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 25 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 27 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 28 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 32 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 33 | github.com/linkedin/goavro/v2 v2.13.1 h1:4qZ5M0QzQFDRqccsroJlgOJznqAS/TpdvXg55h429+I= 34 | github.com/linkedin/goavro/v2 v2.13.1/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 38 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 39 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 40 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 41 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 42 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 43 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 44 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 45 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 46 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 48 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 49 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 51 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 52 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 53 | go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= 54 | go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= 55 | go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= 56 | go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= 57 | go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= 58 | go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= 59 | go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= 60 | go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= 61 | go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= 62 | go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= 63 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 64 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 65 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 66 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 67 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 68 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 69 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 70 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 71 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= 73 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 74 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 75 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= 76 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= 77 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 78 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 79 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 80 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 84 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 85 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 88 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | -------------------------------------------------------------------------------- /installer/installer_test.go: -------------------------------------------------------------------------------- 1 | // Package install contains functions necessary for installing and checking 2 | // if the necessary underlying shared libs have been properly installed 3 | package installer 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/spf13/afero" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestNativeLibPath(t *testing.T) { 16 | lib := NativeLibPath() 17 | 18 | libFilePath := filepath.Join(lib, "lib.go") 19 | file, err := os.ReadFile(libFilePath) 20 | assert.NoError(t, err) 21 | assert.Contains(t, string(file), "-lpact_ffi") 22 | } 23 | 24 | // 1. Be able to specify the path of the binary in advance 25 | // 2. Check if the correct versions of the libs are present??? 26 | // 3. Download the appropriate libs 27 | // 4. Disable the check 28 | 29 | func TestInstallerDownloader(t *testing.T) { 30 | t.Run("generates correct download URLs", func(t *testing.T) { 31 | tests := []struct { 32 | name string 33 | pkg string 34 | want string 35 | test Installer 36 | }{ 37 | { 38 | name: "ffi lib - linux x86_64", 39 | pkg: FFIPackage, 40 | want: func() string { 41 | if checkMusl() { 42 | return fmt.Sprintf("https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v%s/libpact_ffi-linux-x86_64-musl.so.gz", packages[FFIPackage].version) 43 | } else { 44 | return fmt.Sprintf("https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v%s/libpact_ffi-linux-x86_64.so.gz", packages[FFIPackage].version) 45 | } 46 | }(), 47 | test: Installer{ 48 | os: linux, 49 | arch: x86_64, 50 | }, 51 | }, 52 | { 53 | name: "ffi lib - linux aarch64", 54 | pkg: FFIPackage, 55 | want: func() string { 56 | if checkMusl() { 57 | return fmt.Sprintf("https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v%s/libpact_ffi-linux-aarch64-musl.so.gz", packages[FFIPackage].version) 58 | } else { 59 | return fmt.Sprintf("https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v%s/libpact_ffi-linux-aarch64.so.gz", packages[FFIPackage].version) 60 | } 61 | }(), 62 | test: Installer{ 63 | os: linux, 64 | arch: aarch64, 65 | }, 66 | }, 67 | { 68 | name: "ffi lib - macos x86_64", 69 | pkg: FFIPackage, 70 | want: fmt.Sprintf("https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v%s/libpact_ffi-macos-x86_64.dylib.gz", packages[FFIPackage].version), 71 | test: Installer{ 72 | os: macos, 73 | arch: x86_64, 74 | }, 75 | }, 76 | { 77 | name: "ffi lib - macos arm64", 78 | pkg: FFIPackage, 79 | want: fmt.Sprintf("https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v%s/libpact_ffi-macos-aarch64.dylib.gz", packages[FFIPackage].version), 80 | test: Installer{ 81 | os: macos, 82 | arch: aarch64, 83 | }, 84 | }, 85 | { 86 | name: "ffi lib - windows x86_64", 87 | pkg: FFIPackage, 88 | want: fmt.Sprintf("https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v%s/pact_ffi-windows-x86_64.dll.gz", packages[FFIPackage].version), 89 | test: Installer{ 90 | os: windows, 91 | arch: x86_64, 92 | }, 93 | }, 94 | } 95 | 96 | for _, tt := range tests { 97 | t.Run(tt.name, func(t *testing.T) { 98 | src, err := tt.test.getDownloadURLForPackage(tt.pkg) 99 | assert.NoError(t, err) 100 | assert.Equal(t, tt.want, src) 101 | }) 102 | } 103 | }) 104 | 105 | t.Run("downloads the files when libs are not installed", func(t *testing.T) { 106 | mock := &mockDownloader{} 107 | 108 | i, _ := NewInstaller(func(i *Installer) error { 109 | i.downloader = mock 110 | 111 | return nil 112 | }) 113 | 114 | _ = i.downloadDependencies() // This will actually error on the "chmod" if the file doesn't exist 115 | 116 | assert.True(t, mock.called) 117 | }) 118 | 119 | t.Run("checks if existing libraries are present", func(t *testing.T) { 120 | oldPackages := packages 121 | defer func() { packages = oldPackages }() 122 | 123 | packages = map[string]packageInfo{ 124 | FFIPackage: { 125 | libName: "libpact_ffi", 126 | version: "0.0.0", 127 | semverRange: ">= 0.0.0, < 1.0.0", 128 | }, 129 | } 130 | 131 | // TODO: 132 | 133 | }) 134 | 135 | t.Run("errors if installed versions are out of date", func(t *testing.T) { 136 | 137 | }) 138 | 139 | t.Run("errors if installed versions are out of date", func(t *testing.T) { 140 | 141 | }) 142 | } 143 | 144 | func TestInstallerCheckInstallation(t *testing.T) { 145 | t.Run("returns an error when existing libraries aren't present", func(t *testing.T) { 146 | i := &Installer{ 147 | fs: afero.NewMemMapFs(), 148 | downloader: &mockDownloader{}, 149 | hasher: &mockHasher{}, 150 | config: &mockConfiguration{}, 151 | os: "macos", 152 | } 153 | err := i.CheckInstallation() 154 | 155 | assert.Error(t, err) 156 | }) 157 | 158 | t.Run("returns nil when existing libraries are present", func(t *testing.T) { 159 | mockFs := afero.NewMemMapFs() 160 | i := &Installer{ 161 | fs: mockFs, 162 | downloader: &mockDownloader{}, 163 | hasher: &mockHasher{}, 164 | config: &mockConfiguration{}, 165 | os: "macos", 166 | } 167 | 168 | for pkg := range packages { 169 | dst, _ := i.getLibDstForPackage(pkg) 170 | _, _ = mockFs.Create(dst) 171 | } 172 | 173 | err := i.CheckInstallation() 174 | assert.NoError(t, err) 175 | }) 176 | 177 | } 178 | 179 | func TestInstallerCheckPackageInstall(t *testing.T) { 180 | t.Run("downloads and install dependencies when existing libraries aren't present", func(t *testing.T) { 181 | defer restoreMacOSInstallName()() 182 | mockFs := afero.NewMemMapFs() 183 | 184 | var i *Installer 185 | 186 | i = &Installer{ 187 | fs: mockFs, 188 | downloader: &mockDownloader{ 189 | callFunc: func() { 190 | for pkg := range packages { 191 | dst, _ := i.getLibDstForPackage(pkg) 192 | _, _ = mockFs.Create(dst) 193 | } 194 | }, 195 | }, 196 | hasher: &mockHasher{}, 197 | config: &mockConfiguration{}, 198 | os: "macos", 199 | } 200 | 201 | err := i.CheckInstallation() 202 | assert.NoError(t, err) 203 | }) 204 | } 205 | 206 | type mockDownloader struct { 207 | src string 208 | dst string 209 | called bool 210 | callFunc func() 211 | } 212 | 213 | func (m *mockDownloader) download(src, dst string) error { 214 | m.src = src 215 | m.dst = dst 216 | m.called = true 217 | if m.callFunc != nil { 218 | m.callFunc() 219 | } 220 | 221 | return nil 222 | } 223 | 224 | type mockHasher struct { 225 | } 226 | 227 | func (m *mockHasher) hash(src string) (string, error) { 228 | return "1234", nil 229 | } 230 | 231 | type mockConfiguration struct { 232 | } 233 | 234 | func (m *mockConfiguration) readConfig() pactConfig { 235 | return pactConfig{ 236 | Libraries: make(map[string]packageMetadata), 237 | } 238 | } 239 | 240 | func (m *mockConfiguration) writeConfig(pactConfig) error { 241 | return nil 242 | } 243 | 244 | func restoreMacOSInstallName() func() { 245 | old := setMacOSInstallName 246 | setMacOSInstallName = func(string) error { 247 | return nil 248 | } 249 | 250 | return func() { 251 | setMacOSInstallName = old 252 | } 253 | } 254 | 255 | func TestUpdateConfiguration(t *testing.T) { 256 | 257 | } 258 | -------------------------------------------------------------------------------- /internal/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "github.com/pact-foundation/pact-go/v2/installer" 5 | "github.com/pact-foundation/pact-go/v2/internal/native" 6 | ) 7 | 8 | func CheckInstall() error { 9 | // initialised the lib registry. It just needs one of the main lib interfaces Version() here 10 | installer.LibRegistry[installer.FFIPackage] = &native.MockServer{} 11 | 12 | i, err := installer.NewInstaller() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | return i.CheckInstallation() 18 | } 19 | -------------------------------------------------------------------------------- /internal/native/c_types_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || linux 2 | 3 | package native 4 | 5 | import "C" 6 | 7 | type CUlong = C.ulong 8 | -------------------------------------------------------------------------------- /internal/native/c_types_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package native 4 | 5 | import "C" 6 | 7 | type CUlong = C.ulonglong 8 | -------------------------------------------------------------------------------- /internal/native/lib.go: -------------------------------------------------------------------------------- 1 | // Package native contains the c bindings into the Pact Reference types. 2 | package native 3 | 4 | /* 5 | #cgo darwin,arm64 LDFLAGS: -L/tmp -L/usr/local/lib -Wl,-rpath -Wl,/tmp -Wl,-rpath -Wl,/usr/local/lib -lpact_ffi 6 | #cgo darwin,amd64 LDFLAGS: -L/tmp -L/usr/local/lib -Wl,-rpath -Wl,/tmp -Wl,-rpath -Wl,/usr/local/lib -lpact_ffi 7 | #cgo windows,amd64 LDFLAGS: -lpact_ffi 8 | #cgo linux,amd64 LDFLAGS: -L/tmp -L/opt/pact/lib -L/usr/local/lib -Wl,-rpath -Wl,/opt/pact/lib -Wl,-rpath -Wl,/tmp -Wl,-rpath -Wl,/usr/local/lib -lpact_ffi 9 | #cgo linux,arm64 LDFLAGS: -L/tmp -L/opt/pact/lib -L/usr/local/lib -Wl,-rpath -Wl,/opt/pact/lib -Wl,-rpath -Wl,/tmp -Wl,-rpath -Wl,/usr/local/lib -lpact_ffi 10 | */ 11 | import "C" 12 | -------------------------------------------------------------------------------- /internal/native/mismatch.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | // Request is the sub-struct of Mismatch 4 | type Request struct { 5 | Method string `json:"method"` 6 | Path string `json:"path"` 7 | Query string `json:"query,omitempty"` 8 | Headers map[string]string `json:"headers,omitempty"` 9 | Body interface{} `json:"body,omitempty"` 10 | } 11 | 12 | // [ 13 | // { 14 | // "method": "GET", 15 | // "mismatches": [ 16 | // { 17 | // "actual": "", 18 | // "expected": "\"Bearer 1234\"", 19 | // "key": "Authorization", 20 | // "mismatch": "Expected header 'Authorization' but was missing", 21 | // "type": "HeaderMismatch" 22 | // } 23 | // ], 24 | // "path": "/foobar", 25 | // "type": "request-mismatch" 26 | // } 27 | // ] 28 | 29 | // MismatchDetail contains the specific assertions that failed during the verification 30 | type MismatchDetail struct { 31 | Actual string 32 | Expected string 33 | Key string 34 | Mismatch string 35 | Type string 36 | } 37 | 38 | // MismatchedRequest contains details of any request mismatches during pact verification 39 | type MismatchedRequest struct { 40 | Request 41 | Mismatches []MismatchDetail 42 | Type string 43 | } 44 | -------------------------------------------------------------------------------- /internal/native/plugin.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import "fmt" 4 | 5 | // Plugin Errors 6 | var ( 7 | ErrPluginGenericPanic = fmt.Errorf("A general panic was caught.") 8 | ErrPluginMockServerStarted = fmt.Errorf("The mock server has already been started.") 9 | ErrPluginInteractionHandleInvalid = fmt.Errorf("The interaction handle is invalid. ") 10 | ErrPluginInvalidContentType = fmt.Errorf("The content type is not valid.") 11 | ErrPluginInvalidJson = fmt.Errorf("The contents JSON is not valid JSON.") 12 | ErrPluginSpecificError = fmt.Errorf("The plugin returned an error.") 13 | ) 14 | -------------------------------------------------------------------------------- /internal/native/verifier_test.go: -------------------------------------------------------------------------------- 1 | //go:build provider 2 | // +build provider 3 | 4 | package native 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func init() { 14 | Init("INFO") 15 | } 16 | 17 | func TestVerifier_Version(t *testing.T) { 18 | fmt.Println("version: ", Version()) 19 | } 20 | 21 | func TestVerifier_Verify(t *testing.T) { 22 | t.Run("invalid args returns an error", func(t *testing.T) { 23 | 24 | v := Verifier{} 25 | args := []string{ 26 | "--file", 27 | "/non/existent/path.json", 28 | "--hostname", 29 | "localhost", 30 | "--port", 31 | "55827", 32 | "--state-change-url", 33 | "http://localhost:55827/__setup/", 34 | "--loglevel", 35 | "info", 36 | } 37 | 38 | res := v.Verify(args) 39 | 40 | assert.Error(t, res) 41 | }) 42 | } 43 | 44 | func TestVerifier_NewForApplication(t *testing.T) { 45 | v := NewVerifier("pact-go", "test") 46 | 47 | assert.NotNil(t, v.handle) 48 | } 49 | 50 | func TestVerifier_Execute(t *testing.T) { 51 | v := NewVerifier("pact-go", "test") 52 | err := v.Execute() 53 | 54 | assert.NoError(t, err) 55 | } 56 | 57 | func TestVerifier_Shutdown(t *testing.T) { 58 | v := NewVerifier("pact-go", "test") 59 | v.Shutdown() 60 | } 61 | 62 | func TestVerifier_SetProviderInfo(t *testing.T) { 63 | v := NewVerifier("pact-go", "test") 64 | v.SetProviderInfo("name", "http", "localhost", 1234, "/") 65 | } 66 | 67 | func TestVerifier_SetConsumerFilters(t *testing.T) { 68 | v := NewVerifier("pact-go", "test") 69 | v.SetConsumerFilters([]string{"consumer1", "consumer2"}) 70 | } 71 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/hashicorp/logutils" 9 | ) 10 | 11 | var logFilter *logutils.LevelFilter 12 | var defaultLogLevel = "INFO" 13 | 14 | const ( 15 | logLevelTrace logutils.LogLevel = "TRACE" 16 | logLevelDebug logutils.LogLevel = "DEBUG" 17 | logLevelInfo logutils.LogLevel = "INFO" 18 | logLevelWarn logutils.LogLevel = "WARN" 19 | logLevelError logutils.LogLevel = "ERROR" 20 | ) 21 | 22 | func init() { 23 | pactLogLevel := os.Getenv("PACT_LOG_LEVEL") 24 | logLevel := os.Getenv("LOG_LEVEL") 25 | 26 | level := defaultLogLevel 27 | if pactLogLevel != "" { 28 | level = pactLogLevel 29 | } else if logLevel != "" { 30 | level = logLevel 31 | } 32 | 33 | if logFilter == nil { 34 | logFilter = &logutils.LevelFilter{ 35 | Levels: []logutils.LogLevel{logLevelTrace, logLevelDebug, logLevelInfo, logLevelWarn, logLevelError}, 36 | MinLevel: logutils.LogLevel(level), 37 | Writer: os.Stderr, 38 | } 39 | log.SetOutput(logFilter) 40 | log.Println("[DEBUG] initialised logging") 41 | } 42 | } 43 | 44 | // TODO: use the unified logging method to the FFI 45 | 46 | // SetLogLevel sets the default log level for the Pact framework 47 | func SetLogLevel(level logutils.LogLevel) error { 48 | switch level { 49 | case logLevelTrace, logLevelDebug, logLevelError, logLevelInfo, logLevelWarn: 50 | logFilter.SetMinLevel(level) 51 | return nil 52 | default: 53 | return fmt.Errorf(`invalid logLevel '%s'. Please specify one of "TRACE", "DEBUG", "INFO", "WARN", "ERROR"`, level) 54 | } 55 | } 56 | 57 | // LogLevel gets the current log level for the Pact framework 58 | func LogLevel() logutils.LogLevel { 59 | if logFilter != nil { 60 | return logFilter.MinLevel 61 | } 62 | 63 | return logutils.LogLevel(defaultLogLevel) 64 | } 65 | 66 | func PactCrash(err error) { 67 | log.Panicf(crashMessage, err.Error()) 68 | } 69 | 70 | var crashMessage = `!!!!!!!!! PACT CRASHED !!!!!!!!! 71 | 72 | %s 73 | 74 | This is almost certainly a bug in Pact Go. It would be great if you could 75 | open a bug report at: https://github.com/pact-foundation/pact-go/issues 76 | so that we can fix it. 77 | 78 | There is additional debugging information above. If you open a bug report, 79 | please rerun with SetLogLevel('trace') and include the 80 | full output. 81 | 82 | SECURITY WARNING: Before including your log in the issue tracker, make sure you 83 | have removed sensitive info such as login credentials and urls that you don't want 84 | to share with the world. 85 | 86 | We're sorry about this! 87 | ` 88 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/pact-foundation/pact-go/v2/command" 4 | 5 | func main() { 6 | command.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /make/config.mk: -------------------------------------------------------------------------------- 1 | export PATH := $(PWD)/pact/bin:$(PATH) 2 | export PATH 3 | export PACT_BROKER_BASE_URL=http://127.0.0.1 4 | export PACT_BROKER_USERNAME=pact_workshop 5 | export PACT_BROKER_PASSWORD=pact_workshop -------------------------------------------------------------------------------- /message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/pact-foundation/pact-go/v2/models" 5 | ) 6 | 7 | type Body interface{} 8 | type Metadata map[string]interface{} 9 | 10 | // Handler is a provider function that generates a 11 | // message for a Consumer given a Message context (state, description etc.) 12 | type Handler func([]models.ProviderState) (Body, Metadata, error) 13 | type Producer Handler 14 | 15 | // Handlers is a list of handlers ordered by description 16 | type Handlers map[string]Handler 17 | -------------------------------------------------------------------------------- /message/v3/message.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | type Body interface{} 4 | type Metadata map[string]interface{} 5 | 6 | // AsynchronousConsumer receives a message and must be able to parse 7 | // the content 8 | type AsynchronousConsumer func(MessageContents) error 9 | 10 | // V3 Message (Asynchronous only) 11 | type MessageContents struct { 12 | // Message Body 13 | Content Body `json:"contents"` 14 | 15 | // Message metadata 16 | Metadata Metadata `json:"metadata"` 17 | } 18 | 19 | type Config struct { 20 | Consumer string 21 | Provider string 22 | PactDir string 23 | } 24 | -------------------------------------------------------------------------------- /message/v4/asynchronous_message_test.go: -------------------------------------------------------------------------------- 1 | package v4 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/pact-foundation/pact-go/v2/log" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // Sync - no plugin 12 | func TestAsyncTypeSystem(t *testing.T) { 13 | p, _ := NewAsynchronousPact(Config{ 14 | Consumer: "asyncconsumer", 15 | Provider: "asyncprovider", 16 | PactDir: "/tmp/", 17 | }) 18 | _ = log.SetLogLevel("INFO") 19 | 20 | type foo struct { 21 | Foo string `json:"foo"` 22 | } 23 | 24 | err := p.AddAsynchronousMessage(). 25 | Given("some state"). 26 | Given("another state"). 27 | ExpectsToReceive("an important json message"). 28 | WithJSONContent(map[string]string{ 29 | "foo": "bar", 30 | }). 31 | AsType(&foo{}). 32 | ConsumedBy(func(mc AsynchronousMessage) error { 33 | fooMessage := mc.Body.(*foo) 34 | assert.Equal(t, "bar", fooMessage.Foo) 35 | return nil 36 | }). 37 | Verify(t) 38 | 39 | assert.NoError(t, err) 40 | 41 | } 42 | 43 | // Sync - with plugin, but no transport 44 | // TODO: ExecuteTest has been disabled for now, because it's not very useful 45 | func TestAsyncTypeSystem_CsvPlugin_Matcher(t *testing.T) { 46 | csvInteraction := `{ 47 | "request.path": "/reports/report002.csv", 48 | "response.status": "200", 49 | "response.contents": { 50 | "pact:content-type": "text/csv", 51 | "csvHeaders": true, 52 | "column:Name": "matching(type,'Name')", 53 | "column:Number": "matching(number,100)", 54 | "column:Date": "matching(datetime, 'yyyy-MM-dd','2000-01-01')" 55 | } 56 | }` 57 | 58 | p, _ := NewAsynchronousPact(Config{ 59 | Consumer: "asyncconsumer", 60 | Provider: "asyncprovider", 61 | PactDir: "/tmp/", 62 | }) 63 | 64 | // TODO: enable when there is a transport for async to test! 65 | err := p.AddAsynchronousMessage(). 66 | Given("some state"). 67 | ExpectsToReceive("some csv content"). 68 | UsingPlugin(PluginConfig{ 69 | Plugin: "csv", 70 | Version: "0.0.6", 71 | }). 72 | WithContents(csvInteraction, "text/csv"). 73 | // StartTransport("notarealtransport", "127.0.0.1", nil). 74 | ExecuteTest(t, func(m AsynchronousMessage) error { 75 | 76 | fmt.Println("Executing the CSV test", string(m.Contents)) 77 | return nil 78 | }) 79 | assert.NoError(t, err) 80 | } 81 | -------------------------------------------------------------------------------- /message/v4/message.go: -------------------------------------------------------------------------------- 1 | package v4 2 | 3 | type Metadata map[string]interface{} 4 | 5 | // AsynchronousMessage is a representation of a single, unidirectional message 6 | // e.g. MQ, pub/sub, Websocket, Lambda 7 | // AsynchronousMessage is the main implementation of the Pact AsynchronousMessage interface. 8 | type AsynchronousMessage MessageContents 9 | 10 | // AsynchronousConsumer receives a message and must be able to parse 11 | // the content 12 | type AsynchronousConsumer func(AsynchronousMessage) error 13 | 14 | // V3 Message (Asynchronous only) 15 | type MessageContents struct { 16 | // Message Body 17 | Contents []byte 18 | 19 | // Body is the attempt to reify the message body back into a specified type 20 | // Not populated for synchronous messages 21 | Body interface{} `json:"contents"` 22 | 23 | // Message metadata. Currently not populated for synchronous messages 24 | // Metadata Metadata `json:"metadata"` 25 | } 26 | 27 | type Config struct { 28 | Consumer string 29 | Provider string 30 | PactDir string 31 | } 32 | -------------------------------------------------------------------------------- /message/v4/synchronous_message_test.go: -------------------------------------------------------------------------------- 1 | package v4 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pact-foundation/pact-go/v2/log" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSyncTypeSystem_NoPlugin(t *testing.T) { 15 | p, _ := NewSynchronousPact(Config{ 16 | Consumer: "consumer", 17 | Provider: "provider", 18 | }) 19 | // Sync - no plugin 20 | err := p.AddSynchronousMessage("some description"). 21 | Given("some state"). 22 | WithRequest(func(r *SynchronousMessageWithRequestBuilder) { 23 | r.WithJSONContent(map[string]string{"foo": "bar"}) 24 | r.WithMetadata(map[string]string{"meta_request": "meta_request_data"}) 25 | }). 26 | WithResponse(func(r *SynchronousMessageWithResponseBuilder) { 27 | r.WithJSONContent(map[string]string{"foo": "bar"}) 28 | r.WithMetadata(map[string]string{"meta_response": "meta_response_data"}) 29 | }). 30 | ExecuteTest(t, func(m SynchronousMessage) error { 31 | // In this scenario, we have no real transport, so we need to mock/handle both directions 32 | 33 | // e.g. MQ use case -> write to a queue, get a response from another queue 34 | 35 | // What is the user expected to do here? 36 | // m.Request. // inbound -> send to queue 37 | // Poll the queue 38 | // m.Response // response from queue 39 | 40 | // User consumes the request 41 | 42 | return nil 43 | }) 44 | assert.NoError(t, err) 45 | } 46 | 47 | // Sync - with plugin, but no transport 48 | func TestSyncTypeSystem_CsvPlugin_Matcher(t *testing.T) { 49 | p, _ := NewSynchronousPact(Config{ 50 | Consumer: "consumer", 51 | Provider: "provider", 52 | PactDir: "/tmp/", 53 | }) 54 | 55 | csvInteraction := `{ 56 | "request.path": "/reports/report002.csv", 57 | "response.status": "200", 58 | "response.contents": { 59 | "pact:content-type": "text/csv", 60 | "csvHeaders": true, 61 | "column:Name": "matching(type,'Name')", 62 | "column:Number": "matching(number,100)", 63 | "column:Date": "matching(datetime, 'yyyy-MM-dd','2000-01-01')" 64 | } 65 | }` 66 | 67 | err := p.AddSynchronousMessage("some description"). 68 | Given("some state"). 69 | UsingPlugin(PluginConfig{ 70 | Plugin: "csv", 71 | Version: "0.0.6", 72 | }). 73 | WithContents(csvInteraction, "text/csv"). 74 | ExecuteTest(t, func(m SynchronousMessage) error { 75 | fmt.Println("Executing the CSV test") 76 | return nil 77 | }) 78 | 79 | assert.NoError(t, err) 80 | } 81 | func TestSyncTypeSystem_ProtobufPlugin_Matcher_Transport(t *testing.T) { 82 | _ = log.SetLogLevel("INFO") 83 | p, _ := NewSynchronousPact(Config{ 84 | Consumer: "consumer", 85 | Provider: "provider", 86 | PactDir: "/tmp/", 87 | }) 88 | dir, _ := os.Getwd() 89 | path := fmt.Sprintf("%s/../../internal/native/pact_plugin.proto", strings.ReplaceAll(dir, "\\", "/")) 90 | 91 | grpcInteraction := `{ 92 | "pact:proto": "` + path + `", 93 | "pact:proto-service": "PactPlugin/InitPlugin", 94 | "pact:content-type": "application/protobuf", 95 | "request": { 96 | "implementation": "notEmpty('pact-go-driver')", 97 | "version": "matching(semver, '0.0.0')" 98 | }, 99 | "response": { 100 | "catalogue": [ 101 | { 102 | "type": "INTERACTION", 103 | "key": "test" 104 | } 105 | ] 106 | } 107 | }` 108 | 109 | // Sync - with plugin + transport (pass) 110 | err := p.AddSynchronousMessage("some description"). 111 | Given("some state"). 112 | UsingPlugin(PluginConfig{ 113 | Plugin: "protobuf", 114 | Version: "0.5.4", 115 | }). 116 | WithContents(grpcInteraction, "application/protobuf"). 117 | StartTransport("grpc", "127.0.0.1", nil). // For plugin tests, we can't assume if a transport is needed, so this is optional 118 | ExecuteTest(t, func(t TransportConfig, m SynchronousMessage) error { 119 | fmt.Println("Executing a test - this is where you would normally make the gRPC call") 120 | 121 | return nil 122 | }) 123 | 124 | assert.Error(t, err) 125 | // assert.Equal(t, "Did not receive any requests for path 'PactPlugin/InitPlugin'", err) 126 | // TODO:- Work out why we get the following error message 127 | 128 | // Error 129 | // synchronous_message_test.go:130: 130 | // Error Trace: /Users/saf/dev/pact-foundation/pact-go/message/v4/synchronous_message_test.go:130 131 | // Error: Not equal: 132 | // expected: string("Did not receive any requests for path 'PactPlugin/InitPlugin'") 133 | // actual : *errors.errorString(&errors.errorString{s:"pact validation failed: [{Request:{Method: Path:PactPlugin/InitPlugin Query: Headers:map[] Body:} Mismatches:[] Type:}]"}) 134 | // Logs 135 | // 2024-07-04T01:36:33.745740Z DEBUG ThreadId(01) pact_plugin_driver::plugin_manager: Got response: ShutdownMockServerResponse { ok: false, results: [MockServerResult { path: "PactPlugin/InitPlugin", error: "Did not receive any requests for path 'PactPlugin/InitPlugin'", mismatches: [] }] } 136 | } 137 | 138 | // Sync - with plugin + transport (fail) 139 | func TestSyncTypeSystem_ProtobufPlugin_Matcher_Transport_Fail(t *testing.T) { 140 | p, _ := NewSynchronousPact(Config{ 141 | Consumer: "consumer", 142 | Provider: "provider", 143 | PactDir: "/tmp/", 144 | }) 145 | _ = log.SetLogLevel("INFO") 146 | dir, _ := os.Getwd() 147 | path := fmt.Sprintf("%s/../../internal/native/pact_plugin.proto", strings.ReplaceAll(dir, "\\", "/")) 148 | 149 | grpcInteraction := `{ 150 | "pact:proto": "` + path + `", 151 | "pact:proto-service": "PactPlugin/InitPlugin", 152 | "pact:content-type": "application/protobuf", 153 | "request": { 154 | "implementation": "notEmpty('pact-go-driver')", 155 | "version": "matching(semver, '0.0.0')" 156 | }, 157 | "response": { 158 | "catalogue": [ 159 | { 160 | "type": "INTERACTION", 161 | "key": "test" 162 | } 163 | ] 164 | } 165 | }` 166 | 167 | err := p.AddSynchronousMessage("some description"). 168 | Given("some state"). 169 | UsingPlugin(PluginConfig{ 170 | Plugin: "protobuf", 171 | Version: "0.5.4", 172 | }). 173 | WithContents(grpcInteraction, "application/protobuf"). 174 | StartTransport("grpc", "127.0.0.1", nil). // For plugin tests, we can't assume if a transport is needed, so this is optional 175 | ExecuteTest(t, func(t TransportConfig, m SynchronousMessage) error { 176 | fmt.Println("Executing a test - this is where you would normally make the gRPC call") 177 | 178 | return errors.New("bad thing") 179 | }) 180 | 181 | assert.Error(t, err) 182 | } 183 | -------------------------------------------------------------------------------- /message/v4/transport.go: -------------------------------------------------------------------------------- 1 | package v4 2 | 3 | type TransportConfig struct { 4 | Port int 5 | Address string 6 | } 7 | -------------------------------------------------------------------------------- /message/verifier.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/pact-foundation/pact-go/v2/models" 11 | "github.com/pact-foundation/pact-go/v2/proxy" 12 | ) 13 | 14 | type messageVerificationHandlerRequest struct { 15 | Description string `json:"description"` 16 | States []models.ProviderState `json:"providerStates"` 17 | } 18 | 19 | var PACT_MESSAGE_METADATA_HEADER = "PACT_MESSAGE_METADATA" 20 | var PACT_MESSAGE_METADATA_HEADER2 = "Pact-Message-Metadata" 21 | 22 | func appendMetadataToResponseHeaders(metadata Metadata, w http.ResponseWriter) { 23 | if len(metadata) > 0 { 24 | log.Println("[DEBUG] adding message metadata header", metadata) 25 | json, err := json.Marshal(metadata) 26 | if err != nil { 27 | log.Println("[WARN] invalid metadata", metadata, ". Unable to marshal to JSON:", err) 28 | } 29 | log.Println("[TRACE] encoded metadata to JSON:", string(json)) 30 | 31 | encoded := base64.StdEncoding.EncodeToString(json) 32 | log.Println("[TRACE] encoded metadata to base64:", encoded) 33 | 34 | w.Header().Add(PACT_MESSAGE_METADATA_HEADER, encoded) 35 | w.Header().Add(PACT_MESSAGE_METADATA_HEADER2, encoded) 36 | 37 | // Content-Type must match the body content type in the pact. 38 | if metadata["contentType"] != nil { 39 | w.Header().Set("Content-Type", metadata["contentType"].(string)) 40 | } else if metadata["content-type"] != nil { 41 | w.Header().Set("Content-Type", metadata["content-type"].(string)) 42 | } else if metadata["Content-Type"] != nil { 43 | w.Header().Set("Content-Type", metadata["Content-Type"].(string)) 44 | } else { 45 | defaultContentType := "application/json; charset=utf-8" 46 | log.Println("[WARN] no content type (key 'contentType') found in message metadata. Defaulting to", defaultContentType) 47 | w.Header().Set("Content-Type", defaultContentType) 48 | } 49 | } 50 | } 51 | 52 | func CreateMessageHandler(messageHandlers Handlers) proxy.Middleware { 53 | return func(next http.Handler) http.Handler { 54 | 55 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | if r.URL.Path == "/__messages" { 57 | log.Printf("[TRACE] message verification handler") 58 | 59 | // Extract message 60 | var message messageVerificationHandlerRequest 61 | body, err := io.ReadAll(r.Body) 62 | r.Body.Close() 63 | log.Printf("[TRACE] message verification handler received request: %+s, %s", body, r.URL.Path) 64 | 65 | if err != nil { 66 | log.Printf("[ERROR] unable to parse message verification request: %s", err) 67 | w.WriteHeader(http.StatusBadRequest) 68 | return 69 | } 70 | 71 | err = json.Unmarshal(body, &message) 72 | 73 | if err != nil { 74 | log.Printf("[ERROR] unable to parse message verification request: %s", err) 75 | w.WriteHeader(http.StatusBadRequest) 76 | return 77 | } 78 | 79 | // Lookup key in function mapping 80 | f, messageFound := messageHandlers[message.Description] 81 | 82 | if !messageFound { 83 | log.Printf("[ERROR] message handler not found for message description: %v", message.Description) 84 | w.WriteHeader(http.StatusNotFound) 85 | return 86 | } 87 | 88 | // Execute function handler 89 | res, metadata, handlerErr := f(message.States) 90 | 91 | if handlerErr != nil { 92 | log.Printf("[ERROR] error executive message handler %s", err) 93 | w.WriteHeader(http.StatusServiceUnavailable) 94 | return 95 | } 96 | 97 | // Write the body back 98 | appendMetadataToResponseHeaders(metadata, w) 99 | 100 | if bytes, ok := res.([]byte); ok { 101 | log.Println("[DEBUG] checking type of message is []byte") 102 | body = bytes 103 | } else { 104 | log.Println("[DEBUG] message body is not []byte, serialising as JSON") 105 | body, err = json.Marshal(res) 106 | if err != nil { 107 | w.WriteHeader(http.StatusServiceUnavailable) 108 | log.Println("[ERROR] error marshalling object:", err) 109 | return 110 | } 111 | } 112 | 113 | w.WriteHeader(http.StatusOK) 114 | _, err = w.Write(body) 115 | if err != nil { 116 | log.Println("[ERROR] failed to write body response:", err) 117 | } 118 | 119 | return 120 | } 121 | log.Println("[TRACE] skipping message handler for request", r.RequestURI) 122 | 123 | // Pass through to application 124 | next.ServeHTTP(w, r) 125 | 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /models/pact_file.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // SpecificationVersion is used to determine the current specification version 4 | type SpecificationVersion string 5 | 6 | const ( 7 | // V2 spec 8 | V2 SpecificationVersion = "2.0.0" 9 | 10 | // V3 spec 11 | V3 = "3.0.0" 12 | 13 | // V4 spec 14 | V4 = "4.0.0" 15 | ) 16 | -------------------------------------------------------------------------------- /models/provider_state.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ProviderState allows parameters and a description to be passed to the verification process 4 | type ProviderState struct { 5 | Name string `json:"name"` 6 | Parameters map[string]interface{} `json:"params,omitempty"` 7 | } 8 | 9 | // ProviderStateResponse may return values in the state setup 10 | // for the "value from provider state" feature 11 | type ProviderStateResponse map[string]interface{} 12 | 13 | // StateHandler is a provider function that sets up a given state before 14 | // the provider interaction is validated 15 | // It can optionally return a map of key => value (JSON) that may be used 16 | // as values in the verification process 17 | // See https://github.com/pact-foundation/pact-reference/tree/master/rust/pact_verifier_cli#state-change-requests 18 | // https://github.com/pact-foundation/pact-js/tree/feat/v3.0.0#provider-state-injected-values for more 19 | type StateHandler func(setup bool, state ProviderState) (ProviderStateResponse, error) 20 | 21 | // StateHandlers is a list of StateHandler's 22 | type StateHandlers map[string]StateHandler 23 | -------------------------------------------------------------------------------- /provider/consumer_version_selector.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | // ConsumerVersionSelector are the way we specify which pacticipants and 4 | // versions we want to use when configuring verifications 5 | // See https://docs.pact.io/selectors for more 6 | // 7 | // Where a new selector is available in the broker but not yet supported here, 8 | // you may use the UntypedConsumerVersionSelector to pass in arbitrary key/values 9 | // 10 | // Definitive list: https://github.com/pact-foundation/pact_broker/blob/master/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_combinations_spec.rb 11 | type ConsumerVersionSelector struct { 12 | Tag string `json:"tag,omitempty"` 13 | FallbackTag string `json:"fallbackTag,omitempty"` 14 | Latest bool `json:"latest,omitempty"` 15 | Consumer string `json:"consumer,omitempty"` 16 | DeployedOrReleased bool `json:"deployedOrReleased,omitempty"` 17 | Deployed bool `json:"deployed,omitempty"` 18 | Released bool `json:"released,omitempty"` 19 | Environment string `json:"environment,omitempty"` 20 | MainBranch bool `json:"mainBranch,omitempty"` 21 | MatchingBranch bool `json:"matchingBranch,omitempty"` 22 | Branch string `json:"branch,omitempty"` 23 | } 24 | 25 | // Type marker 26 | func (c *ConsumerVersionSelector) IsSelector() { 27 | } 28 | 29 | type UntypedConsumerVersionSelector map[string]interface{} 30 | 31 | // Type marker 32 | func (c *UntypedConsumerVersionSelector) IsSelector() { 33 | } 34 | 35 | type Selector interface { 36 | IsSelector() 37 | } 38 | -------------------------------------------------------------------------------- /provider/transport.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | // Transport configures a way to connect to a given provider 4 | type Transport struct { 5 | Scheme string 6 | Protocol string 7 | Port uint16 8 | Path string 9 | } 10 | -------------------------------------------------------------------------------- /provider/verify_request_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/pact-foundation/pact-go/v2/command" 8 | "github.com/pact-foundation/pact-go/v2/internal/native" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestVerifyRequestValidate(t *testing.T) { 13 | handle := native.NewVerifier("pact-go", command.Version) 14 | 15 | t.Run("local validation", func(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | request *VerifyRequest 19 | err bool 20 | panic bool 21 | }{ 22 | {name: "valid parameters", request: &VerifyRequest{ 23 | PactURLs: []string{"http://localhost:1234/path/to/pact"}, 24 | ProviderBaseURL: "http://localhost:8080", 25 | ProviderStatesSetupURL: "http://localhost:8080/setup", 26 | ProviderVersion: "1.0.0", 27 | }, err: false}, 28 | {name: "no base URL provided", request: &VerifyRequest{ 29 | PactURLs: []string{"http://localhost:1234/path/to/pact"}, 30 | }, err: true, panic: true}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if tt.panic { 35 | assert.Panics(t, (func() { 36 | _ = tt.request.validate(handle) 37 | })) 38 | } else { 39 | err := tt.request.validate(handle) 40 | if tt.err { 41 | assert.Error(t, err) 42 | } else { 43 | assert.NoError(t, err) 44 | } 45 | } 46 | }) 47 | } 48 | 49 | }) 50 | 51 | t.Run("broker integration", func(t *testing.T) { 52 | handle := native.NewVerifier("pact-go", command.Version) 53 | 54 | tests := []struct { 55 | name string 56 | request *VerifyRequest 57 | err bool 58 | }{ 59 | {name: "url without version", request: &VerifyRequest{ 60 | PactURLs: []string{"http://localhost:1234/path/to/pact"}, 61 | ProviderBaseURL: "http://localhost:8080", 62 | BrokerURL: "http://localhost:1234", 63 | }, err: true}, 64 | {name: "broker url without name/version", request: &VerifyRequest{ 65 | BrokerURL: "http://localhost:1234", 66 | ProviderBaseURL: "http://localhost:8080", 67 | ProviderVersion: "1.0.0", 68 | BrokerPassword: "1234", 69 | }, err: true}, 70 | } 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | err := tt.request.validate(handle) 74 | if tt.err { 75 | assert.Error(t, err) 76 | } else { 77 | assert.NoError(t, err) 78 | } 79 | }) 80 | } 81 | }) 82 | 83 | t.Run("consumer version selectors", func(t *testing.T) { 84 | tests := []struct { 85 | name string 86 | request *VerifyRequest 87 | err bool 88 | }{ 89 | {name: "no pacticipant", request: &VerifyRequest{ 90 | PactURLs: []string{"http://localhost:1234/path/to/pact"}, 91 | ProviderBaseURL: "http://localhost:8080", 92 | ConsumerVersionSelectors: []Selector{&ConsumerVersionSelector{}}, 93 | }, err: false}, 94 | {name: "pacticipant only", request: &VerifyRequest{ 95 | PactURLs: []string{"http://localhost:1234/path/to/pact"}, 96 | ProviderBaseURL: "http://localhost:8080", 97 | ConsumerVersionSelectors: []Selector{&ConsumerVersionSelector{Consumer: "foo", Tag: "test"}}, 98 | }, err: false}, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | err := tt.request.validate(handle) 103 | if tt.err { 104 | assert.Error(t, err) 105 | } else { 106 | assert.NoError(t, err) 107 | } 108 | }) 109 | } 110 | }) 111 | } 112 | 113 | func TestVerifyRequest(t *testing.T) { 114 | t.Run("#addPactUrlsFromEnvironment", func(t *testing.T) { 115 | const webhookURL, verificationUrl = "pact_changed_webhook_url", "http://localhost:1234/path/to/pact" 116 | enablePactUrlFunc := func() func() { 117 | const pactUrl = "PACT_URL" 118 | os.Setenv(pactUrl, webhookURL) 119 | return func() { 120 | defer os.Unsetenv(pactUrl) 121 | } 122 | } 123 | tests := []struct { 124 | name string 125 | setup func() (teardown func()) 126 | request *VerifyRequest 127 | expectedSize int 128 | expectedUrls []string 129 | }{ 130 | {name: "with env var and undefined request.PactURLs", 131 | setup: enablePactUrlFunc, 132 | request: &VerifyRequest{}, 133 | expectedUrls: []string{webhookURL}, 134 | }, 135 | {name: "with env var and configured PactURLS", 136 | setup: enablePactUrlFunc, 137 | request: &VerifyRequest{PactURLs: []string{verificationUrl}}, 138 | expectedUrls: []string{verificationUrl, webhookURL}, 139 | }, 140 | {name: "without env var and configured PactURLS", 141 | setup: func() func() { return func() {} }, 142 | request: &VerifyRequest{PactURLs: []string{verificationUrl}}, 143 | expectedUrls: []string{verificationUrl}, 144 | }, 145 | } 146 | for _, tt := range tests { 147 | t.Run(tt.name, func(t *testing.T) { 148 | teardown := tt.setup() 149 | defer teardown() 150 | addPactUrlsFromEnvironment(tt.request) 151 | assert.ElementsMatch(t, tt.request.PactURLs, tt.expectedUrls) 152 | }) 153 | } 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /proxy/http.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "github.com/pact-foundation/pact-go/v2/utils" 15 | ) 16 | 17 | // Middleware is a way to use composition to add functionality 18 | // by intercepting the req/response cycle of the Reverse Proxy. 19 | // Each handler must accept an http.Handler and also return an 20 | // http.Handler, allowing a simple way to chain functionality together 21 | type Middleware func(http.Handler) http.Handler 22 | 23 | // Options for the Reverse Proxy configuration 24 | type Options struct { 25 | 26 | // TargetScheme is one of 'http' or 'https' 27 | TargetScheme string 28 | 29 | // TargetAddress is the host:port component to proxy 30 | TargetAddress string 31 | 32 | // TargetPath is the path on the target to proxy 33 | TargetPath string 34 | 35 | // ProxyPort is the port to make available for proxying 36 | // Defaults to a random port 37 | ProxyPort int 38 | 39 | // Middleware to apply to the Proxy 40 | Middleware []Middleware 41 | 42 | // Internal request prefix for proxy to not rewrite 43 | InternalRequestPathPrefix string 44 | 45 | // Custom TLS Configuration for communicating with a Provider 46 | // Useful when verifying self-signed services, MASSL etc. 47 | CustomTLSConfig *tls.Config 48 | } 49 | 50 | // loggingMiddleware logs requests to the proxy 51 | func loggingMiddleware(next http.Handler) http.Handler { 52 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 | log.Printf("[DEBUG] http reverse proxy received connection from %s on path %s\n", r.RemoteAddr, r.RequestURI) 54 | next.ServeHTTP(w, r) 55 | }) 56 | } 57 | 58 | // chainHandlers takes a set of middleware and joins them together 59 | // into a single Middleware, making it much simpler to compose middleware 60 | // together 61 | func chainHandlers(mw ...Middleware) Middleware { 62 | return func(final http.Handler) http.Handler { 63 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 64 | last := final 65 | for i := len(mw) - 1; i >= 0; i-- { 66 | last = mw[i](last) 67 | } 68 | last.ServeHTTP(w, r) 69 | }) 70 | } 71 | } 72 | 73 | // HTTPReverseProxy provides a default setup for proxying 74 | // internal components within the framework 75 | func HTTPReverseProxy(options Options) (int, error) { 76 | log.Println("[DEBUG] starting new proxy with opts", options) 77 | port := options.ProxyPort 78 | var err error 79 | 80 | url := &url.URL{ 81 | Scheme: options.TargetScheme, 82 | Host: options.TargetAddress, 83 | Path: options.TargetPath, 84 | } 85 | 86 | proxy := createProxy(url, options.InternalRequestPathPrefix) 87 | proxy.Transport = customTransport{tlsConfig: options.CustomTLSConfig} 88 | 89 | if port == 0 { 90 | port, err = utils.GetFreePort() 91 | if err != nil { 92 | log.Println("[ERROR] unable to start reverse proxy server:", err) 93 | return 0, err 94 | } 95 | } 96 | 97 | wrapper := chainHandlers(append(options.Middleware, loggingMiddleware)...) 98 | 99 | log.Println("[DEBUG] starting reverse proxy on port", port) 100 | go func() { 101 | err := http.ListenAndServe(fmt.Sprintf(":%d", port), wrapper(proxy)) 102 | if err != nil { 103 | log.Println("[ERROR] error when starting reverse proxy server:", err) 104 | panic(err) 105 | } 106 | }() 107 | 108 | return port, nil 109 | } 110 | 111 | // https://stackoverflow.com/questions/52986853/how-to-debug-httputil-newsinglehostreverseproxy 112 | // Set the proxy.Transport field to an implementation that dumps the request before delegating to the default transport: 113 | 114 | type customTransport struct { 115 | tlsConfig *tls.Config 116 | } 117 | 118 | func (c customTransport) RoundTrip(r *http.Request) (*http.Response, error) { 119 | b, err := httputil.DumpRequestOut(r, false) 120 | if err != nil { 121 | return nil, err 122 | } 123 | log.Println("[TRACE] proxy outgoing request\n", string(b)) 124 | 125 | transport := &http.Transport{ 126 | Proxy: http.ProxyFromEnvironment, 127 | DialContext: (&net.Dialer{ 128 | Timeout: 30 * time.Second, 129 | KeepAlive: 30 * time.Second, 130 | DualStack: true, 131 | }).DialContext, 132 | MaxIdleConns: 100, 133 | IdleConnTimeout: 90 * time.Second, 134 | TLSHandshakeTimeout: 10 * time.Second, 135 | ExpectContinueTimeout: 1 * time.Second, 136 | } 137 | 138 | if c.tlsConfig != nil { 139 | log.Println("[DEBUG] applying custom TLS config") 140 | transport.TLSClientConfig = c.tlsConfig 141 | } 142 | var DefaultTransport http.RoundTripper = transport 143 | 144 | res, err := DefaultTransport.RoundTrip(r) 145 | if err != nil { 146 | log.Println("[ERROR]", err) 147 | return nil, err 148 | } 149 | b, err = httputil.DumpResponse(res, true) 150 | log.Println("[TRACE] proxied server response\n", string(b)) 151 | 152 | return res, err 153 | } 154 | 155 | // Adapted from https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go 156 | func createProxy(target *url.URL, ignorePrefix string) *httputil.ReverseProxy { 157 | targetQuery := target.RawQuery 158 | director := func(req *http.Request) { 159 | if !strings.HasPrefix(req.URL.Path, ignorePrefix) { 160 | log.Println("[DEBUG] setting proxy to target") 161 | log.Println("[DEBUG] incoming request", req.URL) 162 | req.URL.Scheme = target.Scheme 163 | req.URL.Host = target.Host 164 | req.Host = target.Host 165 | 166 | req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) 167 | log.Println("[DEBUG] outgoing request to target", req.URL) 168 | if targetQuery == "" || req.URL.RawQuery == "" { 169 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 170 | } else { 171 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 172 | } 173 | if _, ok := req.Header["User-Agent"]; !ok { 174 | req.Header.Set("User-Agent", "Pact Go") 175 | } 176 | } else { 177 | log.Println("[DEBUG] setting proxy to internal server") 178 | req.URL.Scheme = "http" 179 | req.URL.Host = "localhost" 180 | req.Host = "localhost" 181 | } 182 | } 183 | return &httputil.ReverseProxy{Director: director} 184 | } 185 | 186 | // From httputil package 187 | // https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go 188 | func singleJoiningSlash(a, b string) string { 189 | aslash := strings.HasSuffix(a, "/") 190 | bslash := strings.HasPrefix(b, "/") 191 | switch { 192 | case aslash && bslash: 193 | return a + b[1:] 194 | case !aslash && !bslash: 195 | return a + "/" + b 196 | } 197 | return a + b 198 | } 199 | -------------------------------------------------------------------------------- /proxy/http_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func dummyHandler(header string) http.HandlerFunc { 10 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 11 | w.Header().Set(header, "true") 12 | }) 13 | } 14 | 15 | func DummyMiddleware(header string) Middleware { 16 | return func(next http.Handler) http.Handler { 17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | w.Header().Set(header, "true") 19 | next.ServeHTTP(w, r) 20 | }) 21 | } 22 | } 23 | 24 | func TestLoggingMiddleware(t *testing.T) { 25 | req, err := http.NewRequest("GET", "/x", nil) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | rr := httptest.NewRecorder() 31 | 32 | loggingMiddleware(dummyHandler("X-Dummy-Handler")).ServeHTTP(rr, req) 33 | 34 | if h := rr.Result().Header.Get("X-Dummy-Handler"); h != "true" { 35 | t.Errorf("expected handler to set the header 'X-Dummy-Handler: true' but got '%v'", 36 | h) 37 | } 38 | } 39 | 40 | func TestChainHandlers(t *testing.T) { 41 | req, err := http.NewRequest("GET", "/health-check", nil) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | rr := httptest.NewRecorder() 47 | 48 | headers := []string{ 49 | "1", 50 | "2", 51 | "3", 52 | "X-Dummy-Handler", 53 | } 54 | mw := []Middleware{ 55 | DummyMiddleware("1"), 56 | DummyMiddleware("2"), 57 | DummyMiddleware("3"), 58 | DummyMiddleware("X-Dummy-Handler"), 59 | } 60 | 61 | middlewareChain := chainHandlers(mw...) 62 | middlewareChain(dummyHandler("X-Dummy-Handler")).ServeHTTP(rr, req) 63 | 64 | for _, h := range headers { 65 | if v := rr.Result().Header.Get(h); v != "true" { 66 | t.Errorf("expected handler to set the header '%v: true' but got '%v'", 67 | h, v) 68 | } 69 | } 70 | } 71 | 72 | func TestHTTPReverseProxy(t *testing.T) { 73 | 74 | // Setup target to proxy 75 | port, err := HTTPReverseProxy(Options{ 76 | Middleware: []Middleware{ 77 | DummyMiddleware("1"), 78 | }, 79 | TargetScheme: "http", 80 | TargetAddress: "127.0.0.1:1234", 81 | }) 82 | 83 | if err != nil { 84 | t.Errorf("unexpected error %v", err) 85 | } 86 | 87 | if port == 0 { 88 | t.Errorf("want non-zero port, got %v", port) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /scripts/build.ps1: -------------------------------------------------------------------------------- 1 | $pactDir = $env:USERPROFILE + "\.pact" 2 | $pactBinDir = $pactDir + "\bin" 3 | $exitCode = 0 4 | 5 | # go build -buildvcs=false -o build/pact-go.exe 6 | go build -o build/pact-go.exe 7 | if ($LastExitCode -ne 0) { 8 | Write-Host "ERROR: Failed to build pact-go" 9 | $exitCode=1 10 | } 11 | 12 | ./build/pact-go.exe -l DEBUG install --libDir $env:TMP 13 | if ($LastExitCode -ne 0) { 14 | Write-Host "ERROR: Failed to install pact-go library to $env:TMP" 15 | $exitCode=1 16 | } 17 | 18 | # Install CLI Tools 19 | if (!(Test-Path -Path $pactBinDir\pact-plugin-cli.exe)) { 20 | Write-Host "--> Creating ${pactDir}" 21 | New-Item -Force -ItemType Directory $pactDir 22 | New-Item -Force -ItemType Directory $pactBinDir 23 | 24 | Write-Host "--> Downloading Pact plugin CLI tools" 25 | $downloadDir = $env:TEMP 26 | $pactPluginCliVersion = "0.1.2" 27 | $url = "https://github.com/pact-foundation/pact-plugins/releases/download/pact-plugin-cli-v${pactPluginCliVersion}/pact-plugin-cli-windows-x86_64.exe.gz" 28 | 29 | Write-Host "Downloading $url" 30 | $gz = "$downloadDir\pact-plugin-cli-windows-x86_64.exe.gz" 31 | if (Test-Path "$gz") { 32 | Remove-Item $gz 33 | } 34 | 35 | $downloader = new-object System.Net.WebClient 36 | $downloader.DownloadFile($url, $gz) 37 | 38 | Write-Host "Extracting $gz" 39 | Add-Type -AssemblyName System.IO.Compression.FileSystem 40 | gzip -d $gz 41 | Move-Item "$downloadDir\pact-plugin-cli-windows-x86_64.exe" "$pactBinDir\pact-plugin-cli.exe" 42 | } 43 | 44 | Write-Host "--> Adding pact binaries to path" 45 | $env:PATH += ";$pactBinDir" 46 | Write-Host $env:PATH 47 | Get-ChildItem $pactBinDir 48 | Write-Host "Added pact binaries to path" 49 | 50 | pact-plugin-cli.exe --version 51 | if ($LastExitCode -ne 0) { 52 | Write-Host "ERROR: Failed to install pact-plugin-cli" 53 | $exitCode=1 54 | } 55 | 56 | pact-plugin-cli.exe -y install https://github.com/mefellows/pact-matt-plugin/releases/tag/v0.1.1 --skip-if-installed 57 | if ($LastExitCode -ne 0) { 58 | Write-Host "ERROR: Failed to install pact-matt-plugin" 59 | $exitCode=1 60 | } 61 | 62 | Write-Host "Done!" 63 | if ($exitCode -ne 0) { 64 | Write-Host "--> Build failed, exiting" 65 | Exit $exitCode 66 | } -------------------------------------------------------------------------------- /scripts/create-pr-to-update-pact-ffi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | : "${1?Please supply the pact-ffi version to upgrade to}" 6 | 7 | FFI_VERSION=$1 8 | TYPE=${2:-fix} 9 | DASHERISED_VERSION=$(echo "${FFI_VERSION}" | sed 's/\./\-/g') 10 | BRANCH_NAME="chore/upgrade-to-pact-ffi-${DASHERISED_VERSION}" 11 | 12 | git checkout master 13 | git checkout installer/installer.go 14 | git pull origin master 15 | 16 | git checkout -b ${BRANCH_NAME} 17 | 18 | cat installer/installer.go | sed "s/version:.*/version: \"${FFI_VERSION}\",/" > tmp-install 19 | mv tmp-install installer/installer.go 20 | 21 | git add installer/installer.go 22 | git commit -m "${TYPE}: update pact-ffi to ${FFI_VERSION}" 23 | git push --set-upstream origin ${BRANCH_NAME} 24 | 25 | gh pr create --title "${TYPE}: update pact-ffi to ${FFI_VERSION}" --fill 26 | 27 | git checkout master 28 | -------------------------------------------------------------------------------- /scripts/dispatch-ffi-released.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script to trigger an update of the pact ffi from pact-foundation/pact-reference to listening repos 4 | # Requires a Github API token with repo scope stored in the 5 | # environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES 6 | 7 | : "${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES:?Please set environment variable GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}" 8 | 9 | if [ -n "$1" ]; then 10 | name="\"${1}\"" 11 | else 12 | echo "name not provided as first param" 13 | exit 1 14 | fi 15 | 16 | if [ -n "$2" ]; then 17 | version="\"${2}\"" 18 | else 19 | echo "name not provided as second param" 20 | exit 1 21 | fi 22 | 23 | repository_slug=$(git remote get-url origin | cut -d':' -f2 | sed 's/\.git//') 24 | 25 | output=$(curl -v https://api.github.com/repos/${repository_slug}/dispatches \ 26 | -H 'Accept: application/vnd.github.everest-preview+json' \ 27 | -H "Authorization: Bearer $GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES" \ 28 | -d "{\"event_type\": \"pact-ffi-released\", \"client_payload\": {\"name\": ${name}, \"version\" : ${version}}}" 2>&1) 29 | 30 | if ! echo "${output}" | grep "HTTP\/.* 204" > /dev/null; then 31 | echo "$output" | sed "s/${GITHUB_ACCESS_TOKEN_FOR_PF_RELEASES}/********/g" 32 | echo "Failed to trigger update" 33 | exit 1 34 | else 35 | echo "Update workflow triggered" 36 | fi 37 | 38 | echo "See https://github.com/${repository_slug}/actions?query=workflow%3A%22Update+Pact+FFI+Library%22" -------------------------------------------------------------------------------- /scripts/examples_consumer.ps1: -------------------------------------------------------------------------------- 1 | $exitCode = 0 2 | # Run integration tests 3 | Write-Host "--> Testing E2E examples" 4 | Write-Host "Running consumer tests" 5 | go test -v -tags=consumer -count=1 github.com/pact-foundation/pact-go/v2/examples/... 6 | if ($LastExitCode -ne 0) { 7 | Write-Host "ERROR: E2E Consumer Tests failed, logging failure" 8 | $exitCode=1 9 | } 10 | 11 | Write-Host "Done!" 12 | if ($exitCode -ne 0) { 13 | Write-Host "--> Build failed, exiting" 14 | Exit $exitCode 15 | } -------------------------------------------------------------------------------- /scripts/examples_provider.ps1: -------------------------------------------------------------------------------- 1 | $exitCode = 0 2 | Write-Host "--> Testing E2E examples" 3 | Write-Host "Running provider tests" 4 | $env:SKIP_PUBLISH='true' 5 | go test -v -timeout=30s -tags=provider -count=1 github.com/pact-foundation/pact-go/v2/examples/... 6 | if ($LastExitCode -ne 0) { 7 | Write-Host "ERROR: E2E Provider Tests failed, logging failure" 8 | $exitCode=1 9 | } 10 | 11 | Write-Host "Done!" 12 | if ($exitCode -ne 0) { 13 | Write-Host "--> Build failed, exiting" 14 | Exit $exitCode 15 | } -------------------------------------------------------------------------------- /scripts/install-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 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 | set -e # Needed for Windows bash, which doesn't read the shebang 9 | 10 | function detect_osarch() { 11 | case $(uname -sm) in 12 | 'Linux x86_64') 13 | os='linux' 14 | arch='x86_64' 15 | ;; 16 | 'Linux aarch64') 17 | os='linux' 18 | arch='aarch64' 19 | ;; 20 | 'Darwin x86' | 'Darwin x86_64') 21 | os='osx' 22 | arch='x86_64' 23 | ;; 24 | 'Darwin arm64') 25 | os='osx' 26 | arch='aarch64' 27 | ;; 28 | CYGWIN*|MINGW32*|MSYS*|MINGW*) 29 | os="windows" 30 | arch='x86_64' 31 | ext='.exe' 32 | ;; 33 | *) 34 | echo "Sorry, you'll need to install the plugin CLI manually." 35 | exit 1 36 | ;; 37 | esac 38 | } 39 | 40 | 41 | VERSION="0.1.3" 42 | detect_osarch 43 | 44 | if [ ! -f ~/.pact/bin/pact-plugin-cli ]; then 45 | echo "--- 🐿 Installing plugins CLI version '${VERSION}' (from tag ${TAG})" 46 | mkdir -p ~/.pact/bin 47 | DOWNLOAD_LOCATION=https://github.com/pact-foundation/pact-plugins/releases/download/pact-plugin-cli-v${VERSION}/pact-plugin-cli-${os}-${arch}${ext}.gz 48 | echo " Downloading from: ${DOWNLOAD_LOCATION}" 49 | curl -L -o ~/.pact/bin/pact-plugin-cli-${os}-${arch}.gz "${DOWNLOAD_LOCATION}" 50 | echo " Downloaded $(file ~/.pact/bin/pact-plugin-cli-${os}-${arch}.gz)" 51 | gunzip -f ~/.pact/bin/pact-plugin-cli-${os}-${arch}.gz 52 | mv ~/.pact/bin/pact-plugin-cli-${os}-${arch} ~/.pact/bin/pact-plugin-cli 53 | chmod +x ~/.pact/bin/pact-plugin-cli 54 | fi -------------------------------------------------------------------------------- /scripts/lib: -------------------------------------------------------------------------------- 1 | LIBDIR=$(dirname "$0") 2 | 3 | function step { 4 | echo "" 5 | echo " -----> $1" 6 | } 7 | 8 | function log { 9 | echo " $1" 10 | } 11 | 12 | function detect_os { 13 | platform='unknown' 14 | unamestr=`uname` 15 | if [[ "$unamestr" == 'Linux' ]]; then 16 | platform='linux' 17 | elif [[ "$OSTYPE" == "darwin"* ]]; then 18 | platform='darwin' 19 | elif [[ "$unamestr" == 'FreeBSD' ]]; then 20 | platform='freebsd' 21 | fi 22 | echo $platform 23 | } 24 | 25 | export PATH="${LIBDIR}/../build/pact/bin:${PATH}" -------------------------------------------------------------------------------- /scripts/pact.ps1: -------------------------------------------------------------------------------- 1 | $pactDir = "$env:APPVEYOR_BUILD_FOLDER\pact" 2 | $exitCode = 0 3 | 4 | # Set environment 5 | if (!($env:GOPATH)) { 6 | $env:GOPATH = "c:\go" 7 | } 8 | $env:PACT_BROKER_PROTO = "http" 9 | $env:PACT_BROKER_URL = "localhost" 10 | $env:PACT_BROKER_USERNAME = "pact_workshop" 11 | $env:PACT_BROKER_PASSWORD = "pact_workshop" 12 | 13 | if (Test-Path "$pactDir") { 14 | Write-Host "-> Deleting old pact directory" 15 | rmdir -Recurse -Force $pactDir 16 | } 17 | 18 | # Install CLI Tools 19 | Write-Host "--> Creating ${pactDir}" 20 | New-Item -Force -ItemType Directory $pactDir 21 | 22 | Write-Host "--> Downloading Latest Ruby binaries)" 23 | $downloadDir = $env:TEMP 24 | $latestRelease = Invoke-WebRequest https://github.com/pact-foundation/pact-ruby-standalone/releases/latest -Headers @{"Accept"="application/json"} 25 | $json = $latestRelease.Content | ConvertFrom-Json 26 | $tag = $json.tag_name 27 | $latestVersion = $tag.Substring(1) 28 | $url = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/$tag/pact-$latestVersion-win32.zip" 29 | 30 | Write-Host "Downloading $url" 31 | $zip = "$downloadDir\pact.zip" 32 | if (Test-Path "$zip") { 33 | Remove-Item $zip 34 | } 35 | 36 | $downloader = new-object System.Net.WebClient 37 | $downloader.DownloadFile($url, $zip) 38 | 39 | Write-Host "Extracting $zip" 40 | Add-Type -AssemblyName System.IO.Compression.FileSystem 41 | [System.IO.Compression.ZipFile]::ExtractToDirectory("$zip", $pactDir) 42 | 43 | Write-Host "Moving binaries into position" 44 | Get-ChildItem $pactDir\pact 45 | 46 | Write-Host "--> Adding pact binaries to path" 47 | $pactBinariesPath = "$pactDir\pact\bin" 48 | $env:PATH += ";$pactBinariesPath" 49 | Write-Host $env:PATH 50 | Get-ChildItem $pactBinariesPath 51 | pact-broker version 52 | 53 | 54 | # Run tests 55 | Write-Host "--> Running tests" 56 | $packages = go list github.com/pact-foundation/pact-go/... | where {$_ -inotmatch 'vendor'} | where {$_ -inotmatch 'examples'} 57 | $curDir=$pwd 58 | 59 | foreach ($package in $packages) { 60 | Write-Host "Running tests for $package" 61 | go test -v $package 62 | if ($LastExitCode -ne 0) { 63 | Write-Host "ERROR: Test failed, logging failure" 64 | $exitCode=1 65 | } 66 | } 67 | 68 | 69 | # Run integration tests 70 | Write-Host "--> Testing E2E examples" 71 | Write-Host "Running consumer tests" 72 | docker compose up -d 73 | go test -tags=consumer -count=1 github.com/pact-foundation/pact-go/examples/... -run TestExample 74 | if ($LastExitCode -ne 0) { 75 | Write-Host "ERROR: Test failed, logging failure" 76 | $exitCode=1 77 | } 78 | 79 | Write-Host "Running provider tests" 80 | go test -tags=provider -count=1 github.com/pact-foundation/pact-go/examples/... -run TestExample 81 | if ($LastExitCode -ne 0) { 82 | Write-Host "ERROR: Test failed, logging failure" 83 | $exitCode=1 84 | } 85 | 86 | # Shutdown 87 | Write-Host "Shutting down any remaining pact processes :)" 88 | Stop-Process -Name ruby 89 | 90 | Write-Host "Done!" 91 | if ($exitCode -ne 0) { 92 | Write-Host "--> Build failed, exiting" 93 | Exit $exitCode 94 | } -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | libDir=$(dirname "$0") 4 | . "${libDir}/lib" 5 | 6 | trap cleanup TERM 7 | 8 | function cleanup() { 9 | log "ERROR generating release, please check your git logs and working tree to ensure things are in order." 10 | } 11 | 12 | step "Releasing Pact Go" 13 | 14 | # Get current versions 15 | log "Finding current version" 16 | version=$(cat command/version.go | egrep -o "v([0-9\.]+)-?([a-zA-Z\-\+\.0-9]+)?") 17 | lastVersion=$(git log --grep='chore(release)' | grep chore | head -n1 | egrep -o "v([0-9\.]+)-?([a-zA-Z\-]+)?") 18 | date=$(date "+%d %B %Y") 19 | 20 | # Check tags 21 | log "Checking if ${version} exists" 22 | tagExists=$(git rev-parse --verify -q ${version}) 23 | if [ $? = 0 ]; then 24 | log "ERROR: tag already exists, this could break API compatibility, exiting." 25 | exit 1 26 | fi 27 | 28 | # Generate changelog 29 | step "Generating changelog" 30 | if [ $? = 0 ]; then 31 | changes=$(git log --pretty=format:' * [%h](https://github.com/pact-foundation/pact-go/commit/%h) - %s (%an, %ad)' ${lastVersion}..HEAD | egrep -v "wip(:|\()" | egrep -v "docs(:|\()" | egrep -v "chore(:|\()" | egrep -v "Merge branch" | egrep -v "test(:|\()") 32 | 33 | log "Updating CHANGELOG" 34 | ed CHANGELOG.md << END 35 | 7i 36 | 37 | ### $version ($date) 38 | $changes 39 | . 40 | w 41 | q 42 | END 43 | 44 | log "Changelog updated" 45 | step "Committing changes" 46 | git reset HEAD 47 | git add CHANGELOG.md 48 | git add command/version.go 49 | git commit -m "chore(release): release ${version}" 50 | 51 | step "Creating tag ${version}" 52 | git tag ${version} -m "chore(release): release ${version}" 53 | 54 | log "Done - check your git logs, and then run 'git push --follow-tags'." 55 | else 56 | log "ERROR: Version ${version} does not exist, exiting." 57 | log "To fix this, ensure RELEASE_VERSION in the Wercker build is 58 | set to the correct tag (https://app.wercker.com/Pact-Foundation/pact-go/environment)" 59 | exit 1 60 | fi 61 | 62 | -------------------------------------------------------------------------------- /scripts/unit.ps1: -------------------------------------------------------------------------------- 1 | $exitCode = 0 2 | 3 | # Run unit tests 4 | Write-Host "--> Running unit tests" 5 | $packages = go list -buildvcs=false ./... | Where-Object {$_ -inotmatch 'vendor'} | Where-Object {$_ -inotmatch 'examples'} 6 | 7 | foreach ($package in $packages) { 8 | Write-Host "Running tests for $package" 9 | go test -race -v $package 10 | if ($LastExitCode -ne 0) { 11 | Write-Host "ERROR: Test failed, logging failure" 12 | $exitCode=1 13 | } 14 | } 15 | 16 | Write-Host "Done!" 17 | if ($exitCode -ne 0) { 18 | Write-Host "--> Build failed, exiting" 19 | Exit $exitCode 20 | } -------------------------------------------------------------------------------- /utils/datetime.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | //! Datetime documentation. These are based on the Java DateTimeFormatter 4 | //! 5 | //! Symbol Meaning Presentation Examples 6 | //! ------ ------- ------------ ------- 7 | //! G era text AD; Anno Domini; A 8 | //! u year year 2004; 04 9 | //! y year-of-era year 2004; 04 10 | //! D day-of-year number 189 11 | //! M/L month-of-year number/text 7; 07; Jul; July; J 12 | //! d day-of-month number 10 13 | //! Q/q quarter-of-year number/text 3; 03; Q3; 3rd quarter 14 | //! Y week-based-year year 1996; 96 15 | //! w week-of-week-based-year number 27 16 | //! W week-of-month number 4 17 | //! E day-of-week text Tue; Tuesday; T 18 | //! e/c localized day-of-week number/text 2; 02; Tue; Tuesday; T 19 | //! F week-of-month number 3 20 | //! a am-pm-of-day text PM 21 | //! h clock-hour-of-am-pm (1-12) number 12 22 | //! K hour-of-am-pm (0-11) number 0 23 | //! k clock-hour-of-am-pm (1-24) number 0 24 | //! H hour-of-day (0-23) number 0 25 | //! m minute-of-hour number 30 26 | //! s second-of-minute number 55 27 | //! S fraction-of-second fraction 978 28 | //! A milli-of-day number 1234 29 | //! n nano-of-second number 987654321 30 | //! N nano-of-day number 1234000000 31 | 32 | //! V time-zone ID zone-id America/Los_Angeles; Z; -08:30 33 | //! z time-zone name zone-name Pacific Standard Time; PST 34 | //! O localized zone-offset offset-O GMT+8; GMT+08:00; UTC-08:00; 35 | //! X zone-offset 'Z' for zero offset-X Z; -08; -0830; -08:30; -083015; -08:30:15; 36 | //! x zone-offset offset-x +0000; -08; -0830; -08:30; -083015; -08:30:15; 37 | //! Z zone-offset offset-Z +0000; -0800; -08:00; 38 | //! ' escape for text delimiter 39 | //! '' single quote literal ' 40 | -------------------------------------------------------------------------------- /utils/json_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "log" 7 | ) 8 | 9 | // Format a JSON document to make comparison easier. 10 | func FormatJSONString(object string) string { 11 | var out bytes.Buffer 12 | err := json.Indent(&out, []byte(object), "", "\t") 13 | if err != nil { 14 | log.Println("[ERROR] failed to format string:", err) 15 | return "" 16 | } 17 | return out.String() 18 | } 19 | 20 | // Format a JSON document for creating Pact files. 21 | func FormatJSONObject(object interface{}) string { 22 | out, err := json.Marshal(object) 23 | if err != nil { 24 | log.Println("[ERROR] failed to encode string to json:", err) 25 | return "" 26 | } 27 | return FormatJSONString(string(out)) 28 | } 29 | 30 | // Checks to see if someone has tried to submit a JSON string 31 | // for an object, which is no longer supported 32 | func IsJSONFormattedObject(stringOrObject interface{}) bool { 33 | switch content := stringOrObject.(type) { 34 | case []byte: 35 | case string: 36 | var obj interface{} 37 | err := json.Unmarshal([]byte(content), &obj) 38 | 39 | if err != nil { 40 | return false 41 | } 42 | 43 | // Check if a map type 44 | if _, ok := obj.(map[string]interface{}); ok { 45 | return true 46 | } 47 | } 48 | 49 | return false 50 | } 51 | -------------------------------------------------------------------------------- /utils/port.go: -------------------------------------------------------------------------------- 1 | // Package utils contains a number of helper / utility functions. 2 | package utils 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // GetFreePort Gets an available port by asking the kernal for a random port 13 | // ready and available for use. 14 | func GetFreePort() (int, error) { 15 | addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") 16 | if err != nil { 17 | return 0, err 18 | } 19 | 20 | l, err := net.ListenTCP("tcp", addr) 21 | if err != nil { 22 | return 0, err 23 | } 24 | defer l.Close() 25 | return l.Addr().(*net.TCPAddr).Port, nil 26 | } 27 | 28 | // FindPortInRange Iterate through CSV or Range of ports to find open port 29 | // Valid inputs are "8081", "8081,8085", "8081-8085". Do not combine 30 | // list and range 31 | func FindPortInRange(s string) (int, error) { 32 | // Take care of csv and single value 33 | if !strings.Contains(s, "-") { 34 | ports := strings.Split(strings.TrimSpace(s), ",") 35 | for _, p := range ports { 36 | i, err := strconv.Atoi(p) 37 | if err != nil { 38 | return 0, err 39 | } 40 | err = checkPort(i) 41 | if err != nil { 42 | continue 43 | } 44 | return i, nil 45 | } 46 | return 0, errors.New("all passed ports are unusable") 47 | } 48 | // Now take care of ranges 49 | ports := strings.Split(strings.TrimSpace(s), "-") 50 | if len(ports) != 2 { 51 | return 0, errors.New("invalid range passed") 52 | } 53 | lower, err := strconv.Atoi(ports[0]) 54 | if err != nil { 55 | return 0, err 56 | } 57 | upper, err := strconv.Atoi(ports[1]) 58 | if err != nil { 59 | return 0, err 60 | } 61 | if upper < lower { 62 | return 0, errors.New("invalid range passed") 63 | } 64 | for i := lower; i <= upper; i++ { 65 | err = checkPort(i) 66 | if err != nil { 67 | continue 68 | } 69 | return i, nil 70 | } 71 | return 0, errors.New("all passed ports are unusable") 72 | } 73 | 74 | func checkPort(p int) error { 75 | s := fmt.Sprintf("localhost:%d", p) 76 | addr, err := net.ResolveTCPAddr("tcp", s) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | l, err := net.ListenTCP("tcp", addr) 82 | if err != nil { 83 | return err 84 | } 85 | defer l.Close() 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /utils/port_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func Test_GetFreePort(t *testing.T) { 9 | port, err := GetFreePort() 10 | 11 | if err != nil { 12 | t.Fatalf("Error: %v", err) 13 | } 14 | 15 | if port <= 0 { 16 | t.Fatalf("Expected a port > 0 to be available, got %d", port) 17 | } 18 | } 19 | 20 | func Test_FindPortInRange(t *testing.T) { 21 | cases := []struct { 22 | description string 23 | s string 24 | port int 25 | errorMsg string 26 | }{ 27 | { 28 | description: "single value", 29 | s: "6667", 30 | port: 6667, 31 | errorMsg: "", 32 | }, 33 | { 34 | description: "csv", 35 | s: "6667,6668,6669", 36 | port: 6667, 37 | errorMsg: "", 38 | }, 39 | { 40 | description: "range", 41 | s: "6668-6669", 42 | port: 6668, 43 | errorMsg: "", 44 | }, 45 | { 46 | description: "invalid single", 47 | s: "abc", 48 | port: 0, 49 | errorMsg: `strconv.Atoi: parsing "abc": invalid syntax`, 50 | }, 51 | { 52 | description: "invalid lower range", 53 | s: "abc-123", 54 | port: 0, 55 | errorMsg: `strconv.Atoi: parsing "abc": invalid syntax`, 56 | }, 57 | { 58 | description: "invalid upper range", 59 | s: "123-abc", 60 | port: 0, 61 | errorMsg: `strconv.Atoi: parsing "abc": invalid syntax`, 62 | }, 63 | { 64 | description: "invalid range", 65 | s: "8888-7777", 66 | port: 0, 67 | errorMsg: "invalid range passed", 68 | }, 69 | { 70 | description: "double range", 71 | s: "6668-6669,7000-7001", 72 | port: 0, 73 | errorMsg: "invalid range passed", 74 | }, 75 | } 76 | 77 | for _, c := range cases { 78 | t.Run(c.description, func(t *testing.T) { 79 | p, err := FindPortInRange(c.s) 80 | if err != nil && err.Error() != c.errorMsg { 81 | t.Fatalf("unexpected error %s", err.Error()) 82 | } else if err == nil && c.errorMsg != "" { 83 | t.Fatalf("expected error %s", c.errorMsg) 84 | } 85 | if p != c.port { 86 | t.Fatalf("Expected port to be %d got %d", c.port, p) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | // Need to differentiate from above cases because this one requires 93 | // us to use a port. Because of this the values must remain the same 94 | func Test_FindPortInRangeWithUsedPorts(t *testing.T) { 95 | cases := []struct { 96 | description string 97 | s string 98 | port int 99 | errorMsg string 100 | }{ 101 | { 102 | description: "all ports used csv", 103 | s: "6667", 104 | port: 0, 105 | errorMsg: "all passed ports are unusable", 106 | }, 107 | { 108 | description: "all ports used range", 109 | s: "6667-6667", 110 | port: 0, 111 | errorMsg: "all passed ports are unusable", 112 | }, 113 | } 114 | for _, c := range cases { 115 | t.Run(c.description, func(t *testing.T) { 116 | s := "localhost:6667" 117 | addr, err := net.ResolveTCPAddr("tcp", s) 118 | if err != nil { 119 | t.Fatalf("Could not resolve address %s in test", s) 120 | } 121 | 122 | l, err := net.ListenTCP("tcp", addr) 123 | if err != nil { 124 | t.Fatalf("Could not bind to port %s in test", s) 125 | } 126 | defer l.Close() 127 | p, err := FindPortInRange(c.s) 128 | if err != nil && err.Error() != c.errorMsg { 129 | t.Fatalf("unexpected error %s", err.Error()) 130 | } else if err == nil && c.errorMsg != "" { 131 | t.Fatalf("expected error %s", c.errorMsg) 132 | } 133 | if p != c.port { 134 | t.Fatalf("Expected port to be %d got %d", c.port, p) 135 | } 136 | }) 137 | } 138 | } 139 | 140 | func Test_checkPort(t *testing.T) { 141 | // Most cases tested above just have this one to test 142 | err := checkPort(-100) 143 | if err == nil { 144 | t.Fatalf("Expected error got none") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/pact-foundation/pact-go/v2/internal/checker" 7 | ) 8 | 9 | // CheckVersion checks if the currently installed version is within semver range 10 | // and will attempt to download the files to the default or configured directory if 11 | // incorrect 12 | func CheckVersion() { 13 | if err := checker.CheckInstall(); err != nil { 14 | log.Fatal("check version failed:", err) 15 | } 16 | 17 | log.Println("[DEBUG] version check completed") 18 | } 19 | --------------------------------------------------------------------------------