├── diagrams
├── workshop_step1.png
├── workshop_step3_pact.png
├── workshop_step4_pact.png
├── workshop_step5_pact.png
├── workshop_step10-broker.png
├── workshop_step2_unit_test.png
├── workshop_step1_sequence_diagram.png
└── workshop_step1_class-sequence-diagram.png
├── renovate.json
├── .gitignore
├── provider
├── cmd
│ └── usersvc
│ │ └── main.go
├── repository
│ └── user.go
├── user_service.go
└── user_service_test.go
├── consumer
└── client
│ ├── cmd
│ └── main.go
│ ├── client_test.go
│ ├── client.go
│ └── client_pact_test.go
├── make
└── config.mk
├── docker-compose.yaml
├── go.mod
├── LICENSE
├── model
└── user.go
├── .github
└── workflows
│ └── test.yml
├── Makefile
├── go.sum
├── LEARNING.md
└── README.md
/diagrams/workshop_step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step1.png
--------------------------------------------------------------------------------
/diagrams/workshop_step3_pact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step3_pact.png
--------------------------------------------------------------------------------
/diagrams/workshop_step4_pact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step4_pact.png
--------------------------------------------------------------------------------
/diagrams/workshop_step5_pact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step5_pact.png
--------------------------------------------------------------------------------
/diagrams/workshop_step10-broker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step10-broker.png
--------------------------------------------------------------------------------
/diagrams/workshop_step2_unit_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step2_unit_test.png
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/diagrams/workshop_step1_sequence_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step1_sequence_diagram.png
--------------------------------------------------------------------------------
/diagrams/workshop_step1_class-sequence-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pact-foundation/pact-workshop-go/HEAD/diagrams/workshop_step1_class-sequence-diagram.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 | /pact
14 | /pacts
15 | log
16 | logs
17 |
18 | # Vendoring directory
19 | /vendor/
20 |
--------------------------------------------------------------------------------
/provider/cmd/usersvc/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 | "net/http"
7 |
8 | "github.com/pact-foundation/pact-workshop-go/provider"
9 | )
10 |
11 | func main() {
12 | mux := provider.GetHTTPHandler()
13 |
14 | ln, err := net.Listen("tcp", ":8080")
15 | if err != nil {
16 | log.Fatal(err)
17 | }
18 | defer ln.Close()
19 |
20 | log.Printf("API starting: port %d (%s)", 8080, ln.Addr())
21 | log.Printf("API terminating: %v", http.Serve(ln, mux))
22 | }
23 |
--------------------------------------------------------------------------------
/consumer/client/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/url"
6 | "time"
7 |
8 | "github.com/pact-foundation/pact-workshop-go/consumer/client"
9 | )
10 |
11 | var token = time.Now().Format("2006-01-02T15:04")
12 |
13 | func main() {
14 | u, _ := url.Parse("http://localhost:8080")
15 | client := &client.Client{
16 | BaseURL: u,
17 | }
18 |
19 | users, err := client.WithToken(token).GetUser(10)
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 | log.Println(users)
24 | }
25 |
--------------------------------------------------------------------------------
/make/config.mk:
--------------------------------------------------------------------------------
1 | SHELL = /bin/bash
2 |
3 | export PATH := $(PWD)/pact/bin:$(PATH)
4 | export PATH
5 | export PROVIDER_NAME = GoUserService
6 | export CONSUMER_NAME = GoAdminService
7 | export PACT_DIR = $(PWD)/pacts
8 | export LOG_DIR = $(PWD)/log
9 | export PACT_BROKER_PROTO = http
10 | export PACT_BROKER_URL = localhost:8081
11 | export PACT_BROKER_USERNAME = pact_workshop
12 | export PACT_BROKER_PASSWORD = pact_workshop
13 | export VERSION_COMMIT?=$(shell git rev-parse HEAD)
14 | export VERSION_BRANCH?=$(shell git rev-parse --abbrev-ref HEAD)
--------------------------------------------------------------------------------
/provider/repository/user.go:
--------------------------------------------------------------------------------
1 | package repository
2 |
3 | import "github.com/pact-foundation/pact-workshop-go/model"
4 |
5 | // UserRepository is an in-memory db representation of our set of users
6 | type UserRepository struct {
7 | Users map[string]*model.User
8 | }
9 |
10 | // GetUsers returns all users in the repository
11 | func (u *UserRepository) GetUsers() []model.User {
12 | var response []model.User
13 |
14 | for _, user := range u.Users {
15 | response = append(response, *user)
16 | }
17 |
18 | return response
19 | }
20 |
21 | // ByUsername finds a user by their username.
22 | func (u *UserRepository) ByUsername(username string) (*model.User, error) {
23 | if user, ok := u.Users[username]; ok {
24 | return user, nil
25 | }
26 | return nil, model.ErrNotFound
27 | }
28 |
29 | // ByID finds a user by their ID
30 | func (u *UserRepository) ByID(ID int) (*model.User, error) {
31 | for _, user := range u.Users {
32 | if user.ID == ID {
33 | return user, nil
34 | }
35 | }
36 | return nil, model.ErrNotFound
37 | }
38 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | postgres:
5 | image: postgres
6 | healthcheck:
7 | test: psql postgres --command "select 1" -U postgres
8 | ports:
9 | - "5432:5432"
10 | environment:
11 | POSTGRES_USER: postgres
12 | POSTGRES_PASSWORD: password
13 | POSTGRES_DB: postgres
14 |
15 | pact-broker:
16 | image: pactfoundation/pact-broker:latest-multi
17 | links:
18 | - postgres
19 | ports:
20 | - 8081:9292
21 | restart: always
22 | environment:
23 | PACT_BROKER_BASIC_AUTH_USERNAME: pact_workshop
24 | PACT_BROKER_BASIC_AUTH_PASSWORD: pact_workshop
25 | PACT_BROKER_DATABASE_USERNAME: postgres
26 | PACT_BROKER_DATABASE_PASSWORD: password
27 | PACT_BROKER_DATABASE_HOST: postgres
28 | PACT_BROKER_DATABASE_NAME: postgres
29 | PACT_BROKER_DATABASE_CONNECT_MAX_RETRIES: "10"
30 | PACT_BROKER_PUBLIC_HEARTBEAT: "true"
31 | PACT_BROKER_WEBHOOK_SCHEME_WHITELIST: http
32 | PACT_BROKER_WEBHOOK_HOST_WHITELIST: host.docker.internal
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/pact-foundation/pact-workshop-go
2 |
3 | go 1.23
4 |
5 | require (
6 | github.com/google/uuid v1.6.0
7 | github.com/pact-foundation/pact-go/v2 v2.4.1
8 | github.com/stretchr/testify v1.10.0
9 | )
10 |
11 | require (
12 | github.com/davecgh/go-spew v1.1.1 // indirect
13 | github.com/hashicorp/go-version v1.7.0 // indirect
14 | github.com/hashicorp/logutils v1.0.0 // indirect
15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
16 | github.com/kr/text v0.2.0 // indirect
17 | github.com/pmezard/go-difflib v1.0.0 // indirect
18 | github.com/spf13/afero v1.12.0 // indirect
19 | github.com/spf13/cobra v1.9.1 // indirect
20 | github.com/spf13/pflag v1.0.6 // indirect
21 | golang.org/x/net v0.34.0 // indirect
22 | golang.org/x/sys v0.29.0 // indirect
23 | golang.org/x/text v0.21.0 // indirect
24 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
25 | google.golang.org/grpc v1.71.0 // indirect
26 | google.golang.org/protobuf v1.36.5 // indirect
27 | gopkg.in/yaml.v3 v3.0.1 // indirect
28 | )
29 |
--------------------------------------------------------------------------------
/consumer/client/client_test.go:
--------------------------------------------------------------------------------
1 | // +build unit
2 |
3 | package client
4 |
5 | import (
6 | "encoding/json"
7 | "fmt"
8 | "testing"
9 |
10 | "net/http"
11 | "net/http/httptest"
12 | "net/url"
13 |
14 | "github.com/pact-foundation/pact-workshop-go/model"
15 | "github.com/stretchr/testify/assert"
16 | )
17 |
18 | func TestClientUnit_GetUser(t *testing.T) {
19 | userID := 10
20 |
21 | // Setup mock server
22 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
23 | assert.Equal(t, req.URL.String(), fmt.Sprintf("/user/%d", userID))
24 | user, _ := json.Marshal(model.User{
25 | FirstName: "Sally",
26 | LastName: "McDougall",
27 | ID: userID,
28 | Type: "admin",
29 | Username: "smcdougall",
30 | })
31 | rw.Write([]byte(user))
32 | }))
33 | defer server.Close()
34 |
35 | // Setup client
36 | u, _ := url.Parse(server.URL)
37 | client := &Client{
38 | BaseURL: u,
39 | }
40 | user, err := client.GetUser(userID)
41 | assert.NoError(t, err)
42 |
43 | // Assert basic fact
44 | assert.Equal(t, user.ID, userID)
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Pact Foundation
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 |
--------------------------------------------------------------------------------
/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "errors"
4 |
5 | // User is a representation of a User. Dah.
6 | type User struct {
7 | FirstName string `json:"firstName" pact:"example=Sally"`
8 | LastName string `json:"lastName" pact:"example=McSmiley Face😀😍"`
9 | Username string `json:"username" pact:"example=sally"`
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=sally"`
28 | Password string `json:"password" pact:"example=badpassword"`
29 | }
30 |
31 | // LoginResponse is the login response API struct.
32 | type LoginResponse struct {
33 | User *User `json:"user"`
34 | }
35 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Pact-Workshop(GoLang)
2 |
3 | on:
4 | push:
5 | branches:
6 | - step11
7 | - master
8 | pull_request:
9 | branches:
10 | - step11
11 | - master
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | defaults:
17 | run:
18 | shell: bash
19 | steps:
20 | - name: Checkout repository
21 | uses: actions/checkout@v4
22 | - name: Set up Go
23 | uses: actions/setup-go@v5
24 | with:
25 | go-version: 1.23
26 | - name: install
27 | run: make install
28 | - name: install_cli
29 | run: make install_cli
30 | - name: consumer unit tests
31 | run: make unit
32 | - name: consumer pact tests
33 | run: make consumer
34 | - uses: KengoTODA/actions-setup-docker-compose@v1
35 | if: ${{ env.ACT }}
36 | name: Install `docker-compose` for use with act
37 | with:
38 | version: '2.24.1'
39 | - name: start pact broker
40 | run: make broker
41 | - name: publish consumer pacts
42 | run: make publish
43 | - name: provider pact tests
44 | run: make provider
45 | - name: provider check safe to deploy
46 | run: make deploy-provider
47 | - name: provider record deployment
48 | run: make record-deploy-provider
49 | - name: consumer check safe to deploy
50 | run: make record-deploy-consumer
51 | - name: consumer check safe to deploy
52 | run: make deploy-consumer
53 | - name: consumer record deployment
54 | run: make record-deploy-consumer
55 |
56 |
--------------------------------------------------------------------------------
/provider/user_service.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 |
7 | "fmt"
8 | "strconv"
9 | "strings"
10 | "time"
11 |
12 | "github.com/google/uuid"
13 | "github.com/pact-foundation/pact-workshop-go/model"
14 | "github.com/pact-foundation/pact-workshop-go/provider/repository"
15 | )
16 |
17 | var userRepository = &repository.UserRepository{
18 | Users: map[string]*model.User{
19 | "sally": &model.User{
20 | FirstName: "Jean-Marie",
21 | LastName: "de La Beaujardière😀😍",
22 | Username: "sally",
23 | Type: "admin",
24 | ID: 10,
25 | },
26 | },
27 | }
28 |
29 | // Crude time-bound "bearer" token
30 | func getAuthToken() string {
31 | return fmt.Sprintf("Bearer %s", time.Now().Format("2006-01-02T15:04"))
32 | }
33 |
34 | // IsAuthenticated checks for a correct bearer token
35 | func WithCorrelationID(h http.HandlerFunc) http.HandlerFunc {
36 | return func(w http.ResponseWriter, r *http.Request) {
37 | uuid := uuid.New()
38 | w.Header().Set("X-Api-Correlation-Id", uuid.String())
39 | h.ServeHTTP(w, r)
40 | }
41 | }
42 |
43 | // IsAuthenticated checks for a correct bearer token
44 | func IsAuthenticated(h http.HandlerFunc) http.HandlerFunc {
45 | return func(w http.ResponseWriter, r *http.Request) {
46 | if r.Header.Get("Authorization") == getAuthToken() {
47 | h.ServeHTTP(w, r)
48 | } else {
49 | w.Header().Set("Content-Type", "application/json")
50 | w.WriteHeader(http.StatusUnauthorized)
51 | }
52 | }
53 | }
54 |
55 | // GetUser fetches a user if authenticated and exists
56 | func GetUser(w http.ResponseWriter, r *http.Request) {
57 | w.Header().Set("Content-Type", "application/json")
58 |
59 | // Get username from path
60 | a := strings.Split(r.URL.Path, "/")
61 | id, _ := strconv.Atoi(a[len(a)-1])
62 |
63 | user, err := userRepository.ByID(id)
64 | if err != nil {
65 | w.WriteHeader(http.StatusNotFound)
66 | } else {
67 | w.WriteHeader(http.StatusOK)
68 | resBody, _ := json.Marshal(user)
69 | w.Write(resBody)
70 | }
71 | }
72 |
73 | // GetUsers fetches all users
74 | func GetUsers(w http.ResponseWriter, r *http.Request) {
75 | w.Header().Set("Content-Type", "application/json")
76 | w.WriteHeader(http.StatusOK)
77 | resBody, _ := json.Marshal(userRepository.GetUsers())
78 | w.Write(resBody)
79 | }
80 |
81 | func commonMiddleware(f http.HandlerFunc) http.HandlerFunc {
82 | return WithCorrelationID(IsAuthenticated(f))
83 | }
84 |
85 | func GetHTTPHandler() *http.ServeMux {
86 | mux := http.NewServeMux()
87 | mux.HandleFunc("/user/", commonMiddleware(GetUser))
88 | mux.HandleFunc("/users/", commonMiddleware(GetUsers))
89 |
90 | return mux
91 | }
92 |
--------------------------------------------------------------------------------
/consumer/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | "net/url"
11 |
12 | "github.com/pact-foundation/pact-workshop-go/model"
13 | )
14 |
15 | // Client is our consumer interface to the Order API
16 | type Client struct {
17 | BaseURL *url.URL
18 | httpClient *http.Client
19 | Token string
20 | }
21 |
22 | // WithToken applies a token to the outgoing request
23 | func (c *Client) WithToken(token string) *Client {
24 | c.Token = token
25 | return c
26 | }
27 |
28 | // GetUser gets a single user from the API
29 | func (c *Client) GetUser(id int) (*model.User, error) {
30 | req, err := c.newRequest("GET", fmt.Sprintf("/user/%d", id), nil)
31 | if err != nil {
32 | return nil, err
33 | }
34 | var user model.User
35 | res, err := c.do(req, &user)
36 |
37 | if res != nil {
38 | switch res.StatusCode {
39 | case http.StatusNotFound:
40 | return nil, ErrNotFound
41 | case http.StatusUnauthorized:
42 | return nil, ErrUnauthorized
43 | }
44 | }
45 |
46 | if err != nil {
47 | return nil, ErrUnavailable
48 | }
49 |
50 | return &user, err
51 |
52 | }
53 |
54 | // GetUsers gets all users from the API
55 | func (c *Client) GetUsers() ([]model.User, error) {
56 | req, err := c.newRequest("GET", "/users", nil)
57 | if err != nil {
58 | return nil, err
59 | }
60 | var users []model.User
61 | _, err = c.do(req, &users)
62 |
63 | return users, err
64 | }
65 |
66 | func (c *Client) newRequest(method, path string, body interface{}) (*http.Request, error) {
67 | rel := &url.URL{Path: path}
68 | u := c.BaseURL.ResolveReference(rel)
69 | var buf io.ReadWriter
70 | if body != nil {
71 | buf = new(bytes.Buffer)
72 | err := json.NewEncoder(buf).Encode(body)
73 | if err != nil {
74 | return nil, err
75 | }
76 | }
77 | req, err := http.NewRequest(method, u.String(), buf)
78 | if err != nil {
79 | return nil, err
80 | }
81 | if body != nil {
82 | req.Header.Set("Content-Type", "application/json")
83 | }
84 | if c.Token != "" {
85 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token))
86 | }
87 | req.Header.Set("Accept", "application/json")
88 | req.Header.Set("User-Agent", "Admin Service")
89 |
90 | return req, nil
91 | }
92 |
93 | func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) {
94 | if c.httpClient == nil {
95 | c.httpClient = http.DefaultClient
96 | }
97 | resp, err := c.httpClient.Do(req)
98 | if err != nil {
99 | return nil, err
100 | }
101 | defer resp.Body.Close()
102 | err = json.NewDecoder(resp.Body).Decode(v)
103 | return resp, err
104 | }
105 |
106 | var (
107 | // ErrNotFound represents a resource not found (404)
108 | ErrNotFound = errors.New("not found")
109 |
110 | // ErrUnauthorized represents a Forbidden (403)
111 | ErrUnauthorized = errors.New("unauthorized")
112 |
113 | ErrUnavailable = errors.New("api unavailable")
114 | )
115 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | include ./make/config.mk
2 |
3 | PACT_GO_VERSION=2.3.0
4 | PACT_DOWNLOAD_DIR=/tmp
5 | ifeq ($(OS),Windows_NT)
6 | PACT_DOWNLOAD_DIR=$$TMP
7 | endif
8 |
9 | install_cli:
10 | @if [ ! -d pact/bin ]; then\
11 | echo "--- Installing Pact CLI dependencies";\
12 | curl -fsSL https://raw.githubusercontent.com/pact-foundation/pact-ruby-standalone/master/install.sh | bash;\
13 | fi
14 |
15 | install:
16 | go install github.com/pact-foundation/pact-go/v2@v$(PACT_GO_VERSION)
17 | pact-go -l DEBUG install --libDir $(PACT_DOWNLOAD_DIR);
18 |
19 | run-consumer:
20 | @go run consumer/client/cmd/main.go
21 |
22 | run-provider:
23 | @go run provider/cmd/usersvc/main.go
24 |
25 | deploy-consumer:
26 | @echo "--- ✅ Checking if we can deploy consumer"
27 | pact/bin/pact-broker can-i-deploy \
28 | --pacticipant $(CONSUMER_NAME) \
29 | --broker-base-url ${PACT_BROKER_PROTO}://$(PACT_BROKER_URL) \
30 | --broker-username $(PACT_BROKER_USERNAME) \
31 | --broker-password $(PACT_BROKER_PASSWORD) \
32 | --version ${VERSION_COMMIT} \
33 | --to-environment production
34 |
35 | deploy-provider:
36 | @echo "--- ✅ Checking if we can deploy provider"
37 | pact/bin/pact-broker can-i-deploy \
38 | --pacticipant $(PROVIDER_NAME) \
39 | --broker-base-url ${PACT_BROKER_PROTO}://$(PACT_BROKER_URL) \
40 | --broker-username $(PACT_BROKER_USERNAME) \
41 | --broker-password $(PACT_BROKER_PASSWORD) \
42 | --version ${VERSION_COMMIT} \
43 | --to-environment production
44 | record-deploy-consumer:
45 | @echo "--- ✅ Recording deployment of consumer"
46 | pact/bin/pact-broker record-deployment \
47 | --pacticipant $(CONSUMER_NAME) \
48 | --broker-base-url ${PACT_BROKER_PROTO}://$(PACT_BROKER_URL) \
49 | --broker-username $(PACT_BROKER_USERNAME) \
50 | --broker-password $(PACT_BROKER_PASSWORD) \
51 | --version ${VERSION_COMMIT} \
52 | --environment production
53 | record-deploy-provider:
54 | @echo "--- ✅ Recording deployment of provider"
55 | pact/bin/pact-broker record-deployment \
56 | --pacticipant $(PROVIDER_NAME) \
57 | --broker-base-url ${PACT_BROKER_PROTO}://$(PACT_BROKER_URL) \
58 | --broker-username $(PACT_BROKER_USERNAME) \
59 | --broker-password $(PACT_BROKER_PASSWORD) \
60 | --version ${VERSION_COMMIT} \
61 | --environment production
62 |
63 | publish:
64 | @echo "--- 📝 Publishing Pacts"
65 | pact/bin/pact-broker publish ${PWD}/pacts --consumer-app-version ${VERSION_COMMIT} --branch ${VERSION_BRANCH} \
66 | -b $(PACT_BROKER_PROTO)://$(PACT_BROKER_URL) -u ${PACT_BROKER_USERNAME} -p ${PACT_BROKER_PASSWORD}
67 | @echo
68 | @echo "Pact contract publishing complete!"
69 | @echo
70 | @echo "Head over to $(PACT_BROKER_PROTO)://$(PACT_BROKER_URL) and login with $(PACT_BROKER_USERNAME)/$(PACT_BROKER_PASSWORD)"
71 | @echo "to see your published contracts. "
72 | unit:
73 | @echo "--- 🔨Running Unit tests "
74 | go test -tags=unit -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientUnit'
75 |
76 | consumer: export PACT_TEST := true
77 | consumer:
78 | @echo "--- 🔨Running Consumer Pact tests "
79 | go test -tags=integration -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientPact' -v
80 |
81 | provider: export PACT_TEST := true
82 | provider:
83 | @echo "--- 🔨Running Provider Pact tests "
84 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
85 |
86 | broker:
87 | docker compose up -d
88 | .PHONY: deploy-consumer deploy-provider publish unit consumer provider
89 |
--------------------------------------------------------------------------------
/provider/user_service_test.go:
--------------------------------------------------------------------------------
1 | package provider
2 |
3 | import (
4 | "fmt"
5 | l "log"
6 | "net"
7 | "net/http"
8 | "os"
9 | "testing"
10 |
11 | "github.com/pact-foundation/pact-go/v2/log"
12 | "github.com/pact-foundation/pact-go/v2/models"
13 | "github.com/pact-foundation/pact-go/v2/provider"
14 | "github.com/pact-foundation/pact-go/v2/utils"
15 | "github.com/pact-foundation/pact-workshop-go/model"
16 | "github.com/pact-foundation/pact-workshop-go/provider/repository"
17 | )
18 |
19 | // The Provider verification
20 | func TestPactProvider(t *testing.T) {
21 | log.SetLogLevel("INFO")
22 |
23 | go startInstrumentedProvider()
24 |
25 | verifier := provider.NewVerifier()
26 |
27 | // Verify the Provider - Branch-based Published Pacts for any known consumers
28 | err := verifier.VerifyProvider(t, provider.VerifyRequest{
29 | Provider: "GoUserService",
30 | ProviderBaseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
31 | ProviderBranch: os.Getenv("VERSION_BRANCH"),
32 | FailIfNoPactsFound: false,
33 | // Use this if you want to test without the Pact Broker
34 | // PactFiles: []string{filepath.FromSlash(fmt.Sprintf("%s/GoAdminService-GoUserService.json", os.Getenv("PACT_DIR")))},
35 | BrokerURL: fmt.Sprintf("%s://%s", os.Getenv("PACT_BROKER_PROTO"), os.Getenv("PACT_BROKER_URL")),
36 | BrokerUsername: os.Getenv("PACT_BROKER_USERNAME"),
37 | BrokerPassword: os.Getenv("PACT_BROKER_PASSWORD"),
38 | PublishVerificationResults: true,
39 | ProviderVersion: os.Getenv("VERSION_COMMIT"),
40 | StateHandlers: stateHandlers,
41 | RequestFilter: fixBearerToken,
42 | BeforeEach: func() error {
43 | userRepository = sallyExists
44 | return nil
45 | },
46 | })
47 |
48 | if err != nil {
49 | t.Log(err)
50 | }
51 | }
52 |
53 | // Simulates the need to set a time-bound authorization token,
54 | // such as an OAuth bearer token
55 | func fixBearerToken(next http.Handler) http.Handler {
56 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
57 | // Only set the correct bearer token, if one was provided in the first place
58 | if r.Header.Get("Authorization") != "" {
59 | r.Header.Set("Authorization", getAuthToken())
60 | }
61 | next.ServeHTTP(w, r)
62 | })
63 | }
64 |
65 | var stateHandlers = models.StateHandlers{
66 | "User sally exists": func(setup bool, s models.ProviderState) (models.ProviderStateResponse, error) {
67 | userRepository = sallyExists
68 | return models.ProviderStateResponse{}, nil
69 | },
70 | "User sally does not exist": func(setup bool, s models.ProviderState) (models.ProviderStateResponse, error) {
71 | userRepository = sallyDoesNotExist
72 | return models.ProviderStateResponse{}, nil
73 | },
74 | }
75 |
76 | // Starts the provider API with hooks for provider states.
77 | // This essentially mirrors the main.go file, with extra routes added.
78 | func startInstrumentedProvider() {
79 | mux := GetHTTPHandler()
80 |
81 | ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
82 | if err != nil {
83 | l.Fatal(err)
84 | }
85 | defer ln.Close()
86 |
87 | l.Printf("API starting: port %d (%s)", port, ln.Addr())
88 | l.Printf("API terminating: %v", http.Serve(ln, mux))
89 |
90 | }
91 |
92 | // Configuration / Test Data
93 | var dir, _ = os.Getwd()
94 | var pactDir = fmt.Sprintf("%s/../../pacts", dir)
95 | var logDir = fmt.Sprintf("%s/log", dir)
96 | var port, _ = utils.GetFreePort()
97 |
98 | // Provider States data sets
99 | var sallyExists = &repository.UserRepository{
100 | Users: map[string]*model.User{
101 | "sally": &model.User{
102 | FirstName: "Jean-Marie",
103 | LastName: "de La Beaujardière😀😍",
104 | Username: "sally",
105 | Type: "admin",
106 | ID: 10,
107 | },
108 | },
109 | }
110 |
111 | var sallyDoesNotExist = &repository.UserRepository{}
112 |
113 | var sallyUnauthorized = &repository.UserRepository{
114 | Users: map[string]*model.User{
115 | "sally": &model.User{
116 | FirstName: "Jean-Marie",
117 | LastName: "de La Beaujardière😀😍",
118 | Username: "sally",
119 | Type: "blocked",
120 | ID: 10,
121 | },
122 | },
123 | }
124 |
--------------------------------------------------------------------------------
/consumer/client/client_pact_test.go:
--------------------------------------------------------------------------------
1 | //go:build integration
2 |
3 | package client
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "strconv"
9 | "testing"
10 |
11 | "net/url"
12 |
13 | "github.com/pact-foundation/pact-go/v2/consumer"
14 | "github.com/pact-foundation/pact-go/v2/log"
15 | "github.com/pact-foundation/pact-go/v2/matchers"
16 | "github.com/pact-foundation/pact-workshop-go/model"
17 | "github.com/stretchr/testify/assert"
18 | )
19 |
20 | var Like = matchers.Like
21 | var EachLike = matchers.EachLike
22 | var Term = matchers.Term
23 | var Regex = matchers.Regex
24 | var HexValue = matchers.HexValue
25 | var Identifier = matchers.Identifier
26 | var IPAddress = matchers.IPAddress
27 | var IPv6Address = matchers.IPv6Address
28 | var Timestamp = matchers.Timestamp
29 | var Date = matchers.Date
30 | var Time = matchers.Time
31 | var UUID = matchers.UUID
32 | var ArrayMinLike = matchers.ArrayMinLike
33 |
34 | type S = matchers.S
35 | type Map = matchers.MapMatcher
36 |
37 | var u *url.URL
38 | var client *Client
39 |
40 | func TestClientPact_GetUser(t *testing.T) {
41 |
42 | log.SetLogLevel("INFO")
43 | mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{
44 | Consumer: os.Getenv("CONSUMER_NAME"),
45 | Provider: os.Getenv("PROVIDER_NAME"),
46 | LogDir: os.Getenv("LOG_DIR"),
47 | PactDir: os.Getenv("PACT_DIR"),
48 | })
49 | assert.NoError(t, err)
50 |
51 | t.Run("the user exists", func(t *testing.T) {
52 | id := 10
53 |
54 | err = mockProvider.
55 | AddInteraction().
56 | Given("User sally exists").
57 | UponReceiving("A request to login with user 'sally'").
58 | WithRequestPathMatcher("GET", Regex("/user/"+strconv.Itoa(id), "/user/[0-9]+"), func(b *consumer.V2RequestBuilder) {
59 | b.Header("Authorization", Like("Bearer 2019-01-01"))
60 | }).
61 | WillRespondWith(200, func(b *consumer.V2ResponseBuilder) {
62 | b.BodyMatch(model.User{}).
63 | Header("Content-Type", Term("application/json", `application\/json`)).
64 | Header("X-Api-Correlation-Id", Like("100"))
65 | }).
66 | ExecuteTest(t, func(config consumer.MockServerConfig) error {
67 | // Act: test our API client behaves correctly
68 |
69 | // Get the Pact mock server URL
70 | u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))
71 |
72 | // Initialise the API client and point it at the Pact mock server
73 | client = &Client{
74 | BaseURL: u,
75 | }
76 |
77 | // // Execute the API client
78 | user, err := client.WithToken("2019-01-01").GetUser(id)
79 |
80 | // // Assert basic fact
81 | if user.ID != id {
82 | return fmt.Errorf("wanted user with ID %d but got %d", id, user.ID)
83 | }
84 |
85 | return err
86 | })
87 |
88 | assert.NoError(t, err)
89 |
90 | })
91 |
92 | t.Run("the user does not exist", func(t *testing.T) {
93 | id := 10
94 |
95 | err = mockProvider.
96 | AddInteraction().
97 | Given("User sally does not exist").
98 | UponReceiving("A request to login with user 'sally'").
99 | WithRequestPathMatcher("GET", Regex("/user/"+strconv.Itoa(id), "/user/[0-9]+"), func(b *consumer.V2RequestBuilder) {
100 | b.Header("Authorization", Like("Bearer 2019-01-01"))
101 | }).
102 | WillRespondWith(404, func(b *consumer.V2ResponseBuilder) {
103 | b.Header("Content-Type", Term("application/json", `application\/json`)).
104 | Header("X-Api-Correlation-Id", Like("100"))
105 | }).
106 | ExecuteTest(t, func(config consumer.MockServerConfig) error {
107 | // Act: test our API client behaves correctly
108 |
109 | // Get the Pact mock server URL
110 | u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))
111 |
112 | // Initialise the API client and point it at the Pact mock server
113 | client = &Client{
114 | BaseURL: u,
115 | }
116 |
117 | // // Execute the API client
118 | _, err := client.WithToken("2019-01-01").GetUser(id)
119 | assert.Equal(t, ErrNotFound, err)
120 | return nil
121 | })
122 | assert.NoError(t, err)
123 |
124 | })
125 | t.Run("not authenticated", func(t *testing.T) {
126 | id := 10
127 |
128 | err = mockProvider.
129 | AddInteraction().
130 | Given("User is not authenticated").
131 | UponReceiving("A request to login with user 'sally'").
132 | WithRequestPathMatcher("GET", Regex("/user/"+strconv.Itoa(id), "/user/[0-9]+")).
133 | WillRespondWith(401, func(b *consumer.V2ResponseBuilder) {
134 | b.Header("Content-Type", Term("application/json", `application\/json`)).
135 | Header("X-Api-Correlation-Id", Like("100"))
136 | }).
137 | ExecuteTest(t, func(config consumer.MockServerConfig) error {
138 | // Act: test our API client behaves correctly
139 |
140 | // Get the Pact mock server URL
141 | u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))
142 |
143 | // Initialise the API client and point it at the Pact mock server
144 | client = &Client{
145 | BaseURL: u,
146 | }
147 |
148 | // // Execute the API client
149 | _, err := client.WithToken("").GetUser(id)
150 | assert.Equal(t, ErrUnauthorized, err)
151 | return nil
152 | })
153 | assert.NoError(t, err)
154 | })
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
6 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
7 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
8 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
9 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
10 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
11 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
12 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
13 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
14 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
15 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
16 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
17 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
18 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
19 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
20 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
21 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
22 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25 | github.com/pact-foundation/pact-go/v2 v2.3.0 h1:bpeGxey9LK0AZ4UB1JFtP2LyuuPXAP5ynb2wWbjRIeQ=
26 | github.com/pact-foundation/pact-go/v2 v2.3.0/go.mod h1:mHMp9jeFlk7l7Afp2ePUUVpKltLxbW5XO5y9XDRpfgk=
27 | github.com/pact-foundation/pact-go/v2 v2.4.1 h1:eaLC58qzeCTbwdlCY8UvWz1HmDW+qrjTFfH8Xoq0rWs=
28 | github.com/pact-foundation/pact-go/v2 v2.4.1/go.mod h1:OwnXXRliPZvKDMJn/IsAwQ95tQprmp5gPTzPYz54mTg=
29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
32 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
33 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
34 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
35 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
36 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
37 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
38 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
39 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
40 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
41 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
42 | go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
43 | go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
44 | go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
45 | go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
46 | go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
47 | go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
48 | go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
49 | go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
50 | go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
51 | go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
52 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
53 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
54 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
55 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
56 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
57 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
58 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
59 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
60 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
61 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
62 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
64 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
65 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
66 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
67 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
68 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
69 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
71 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
72 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
73 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
74 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
75 |
--------------------------------------------------------------------------------
/LEARNING.md:
--------------------------------------------------------------------------------
1 | # Learning Outcomes
2 |
3 |
4 | | Step | Title | Concept Covered | Learning objectives | Further Reading |
5 | |----------------------------------------------------------------------|---------------------------------------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
6 | | [step 1](//github.com/pact-foundation/pact-workshop-go/tree/step1) | Create our consumer before the Provider API even exists | Consumer-driven design |
- Understand use case
| - https://martinfowler.com/articles/consumerDrivenContracts.html
| |
7 | | [step 2](//github.com/pact-foundation/pact-workshop-go/tree/step2) | Write a unit test for our consumer | - | - How to write a basic unit test for an HTTP Client
- Understand how a unit test is unable to catch certain integration issues
| - https://docs.pact.io/faq/convinceme
|
8 | | [step 3](//github.com/pact-foundation/pact-workshop-go/tree/step3) | Write a Pact test for our consumer | Consumer side pact test | - Understand basic Consumer-side Pact concepts
- Understand "Matchers" to avoid test data brittleness
- Demonstrate that Pact tests are able to catch a class of integration problems
| - https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-consumer-pact-test
- https://docs.pact.io/best_practices/consumer
| |
9 | | [step 4](//github.com/pact-foundation/pact-workshop-go/tree/step4) | Verify the consumer pact with the Provider API | Provider side pact test | - Understand basic Provider-side Pact concepts
- Place provider side testing in a broader testing context (e.g. where it fits on the pyramid)
| - https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test
|
10 | | [step 5](//github.com/pact-foundation/pact-workshop-go/tree/step5) | Fix the consumer's bad assumptions about the Provider | Humans talking to humans (collaboration) | - Understand that a tool facilitates collaboration, it doesn't replace it
| |
11 | | [step 6](//github.com/pact-foundation/pact-workshop-go/tree/step6) | Write a pact test for `404` (missing User) in consumer | Testing API invariants | - Understand how we can test "stateful" APIs without having to create complex, sequential API calls
| |
12 | | [step 7](//github.com/pact-foundation/pact-workshop-go/tree/step7) | Update API to handle `404` case | Provider States | - How Pact deals with "stateful" interactions, via the concept of "Provider States"
| - https://docs.pact.io/getting_started/provider_states
|
13 | | [step 8](//github.com/pact-foundation/pact-workshop-go/tree/step8) | Write a pact test for the `401` case | Testing authenticated APIs | - Service evolution - adding a new feature
| |
14 | | [step 9](//github.com/pact-foundation/pact-workshop-go/tree/step9) | Update API to handle `401` case | Service evolution | - Understand the challenge of dealing with complex/dynamic data, such as time-bound authentication tokens
| |
15 | | [step 10](//github.com/pact-foundation/pact-workshop-go/tree/step10) | Fix the provider to support the `401` case | Request filters | - Understand Pact's approach to dealing with complex/dynamic data, such as time-bound authentication tokens
| |
16 | | [step 11](//github.com/pact-foundation/pact-workshop-go/tree/step11) | Implement a broker workflow for integration with CI/CD | Automation | - Understand how to use Pact in a CI and CD workflow
| - https://docs.pact.io/pact_broker
- https://docs.pact.io/best_practices/pact_nirvana
|
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pact Go workshop
2 |
3 | ## Introduction
4 | This workshop is aimed at demonstrating core features and benefits of contract testing with Pact.
5 |
6 | Whilst contract testing can be applied retrospectively to systems, we will follow the [consumer driven contracts](https://martinfowler.com/articles/consumerDrivenContracts.html) approach in this workshop - where a new consumer and provider are created in parallel to evolve a service over time, especially where there is some uncertainty with what is to be built.
7 |
8 | This workshop should take from 1 to 2 hours, depending on how deep you want to go into each topic.
9 |
10 | **Workshop outline**:
11 |
12 | - [step 1: **create consumer**](//github.com/pact-foundation/pact-workshop-go/tree/step1): Create our consumer before the Provider API even exists
13 | - [step 2: **unit test**](//github.com/pact-foundation/pact-workshop-go/tree/step2): Write a unit test for our consumer
14 | - [step 3: **pact test**](//github.com/pact-foundation/pact-workshop-go/tree/step3): Write a Pact test for our consumer
15 | - [step 4: **pact verification**](//github.com/pact-foundation/pact-workshop-go/tree/step4): Verify the consumer pact with the Provider API
16 | - [step 5: **fix consumer**](//github.com/pact-foundation/pact-workshop-go/tree/step5): Fix the consumer's bad assumptions about the Provider
17 | - [step 6: **pact test**](//github.com/pact-foundation/pact-workshop-go/tree/step6): Write a pact test for `404` (missing User) in consumer
18 | - [step 7: **provider states**](//github.com/pact-foundation/pact-workshop-go/tree/step7): Update API to handle `404` case
19 | - [step 8: **pact test**](//github.com/pact-foundation/pact-workshop-go/tree/step8): Write a pact test for the `401` case
20 | - [step 9: **pact test**](//github.com/pact-foundation/pact-workshop-go/tree/step9): Update API to handle `401` case
21 | - [step 10: **request filters**](//github.com/pact-foundation/pact-workshop-go/tree/step10): Fix the provider to support the `401` case
22 | - [step 11: **pact broker**](//github.com/pact-foundation/pact-workshop-go/tree/step11): Implement a broker workflow for integration with CI/CD
23 |
24 | _NOTE: Each step is tied to, and must be run within, a git branch, allowing you to progress through each stage incrementally. For example, to move to step 2 run the following: `git checkout step2`_
25 |
26 | ## Learning objectives
27 |
28 | If running this as a team workshop format, you may want to take a look through the [learning objectives](./LEARNING.md).
29 |
30 | ## Scenario
31 |
32 | There are two components in scope for our workshop.
33 |
34 | 1. Admin Service (Consumer). Does Admin-y things, and often needs to communicate to the User service. But really, it's just a placeholder for a more useful consumer (e.g. a website or another microservice) - it doesn't do much!
35 | 1. User Service (Provider). Provides useful things about a user, such as listing all users and getting the details of individuals.
36 |
37 | For the purposes of this workshop, we won't implement any functionality of the Admin Service, except the bits that require User information.
38 |
39 | **Project Structure**
40 |
41 | The key packages are shown below:
42 |
43 | ```sh
44 | ├── consumer # Contains the Admin Service Team (client) project
45 | ├── model # Shared domain model
46 | ├── pact # The directory of the Pact Standalone CLI
47 | ├── provider # The User Service Team (provider) project
48 | ```
49 |
50 | ## Step 1 - Simple Consumer calling Provider
51 |
52 | We need to first create an HTTP client to make the calls to our provider service:
53 |
54 | 
55 |
56 | *NOTE*: even if the API client had been been graciously provided for us by our Provider Team, it doesn't mean that we shouldn't write contract tests - because the version of the client we have may not always be in sync with the deployed API - and also because we will write tests on the output appropriate to our specific needs.
57 |
58 | This User Service expects a `users` path parameter, and then returns some simple json back:
59 |
60 | 
61 |
62 | You can see the client public interface we created in the `consumer/client` package:
63 |
64 | ```go
65 |
66 | type Client struct {
67 | BaseURL *url.URL
68 | httpClient *http.Client
69 | }
70 |
71 | // GetUser gets a single user from the API
72 | func (c *Client) GetUser(id int) (*model.User, error) {
73 | }
74 | ```
75 |
76 | We can run the client with `make run-consumer` - it should fail with an error, because the Provider is not running.
77 |
78 | *Move on to [step 2](//github.com/pact-foundation/pact-workshop-go/tree/step2): Write a unit test for our consumer*
79 |
80 | ## Step 2 - Client Tested but integration fails
81 |
82 | Now lets create a basic test for our API client. We're going to check 2 things:
83 |
84 | 1. That our client code hit the expected endpoint
85 | 1. That the response is marshalled into a `User` object, with the correct ID
86 |
87 | *consumer/client/client_test.go*
88 |
89 | ```go
90 | func TestClientUnit_GetUser(t *testing.T) {
91 | userID := 10
92 |
93 | // Setup mock server
94 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
95 | assert.Equal(t, req.URL.String(), fmt.Sprintf("/users/%d", userID))
96 | user, _ := json.Marshal(model.User{
97 | FirstName: "Sally",
98 | LastName: "McDougall",
99 | ID: userID,
100 | Type: "admin",
101 | Username: "smcdougall",
102 | })
103 | rw.Write([]byte(user))
104 | }))
105 | defer server.Close()
106 |
107 | // Setup client
108 | u, _ := url.Parse(server.URL)
109 | client := &Client{
110 | BaseURL: u,
111 | }
112 | user, err := client.GetUser(userID)
113 | assert.NoError(t, err)
114 |
115 | // Assert basic fact
116 | assert.Equal(t, user.ID, userID)
117 | }
118 |
119 | ```
120 |
121 | 
122 |
123 | Let's run this spec and see it all pass:
124 |
125 | ```
126 | $ make unit
127 |
128 | --- 🔨Running Unit tests
129 | go test -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientUnit'
130 | ok github.com/pact-foundation/pact-workshop-go/consumer/client 10.196s
131 | ```
132 |
133 | Meanwhile, our provider team has started building out their API in parallel. Let's run our client against our provider (you'll need two terminals to do this):
134 |
135 |
136 | ```
137 | # Terminal 1
138 | $ make run-provider
139 |
140 | 2019/10/28 18:24:37 API starting: port 8080 ([::]:8080)
141 |
142 | # Terminal 2
143 | make run-consumer
144 |
145 | 2019/10/28 18:25:57 api unavailable
146 | exit status 1
147 | make: *** [run-consumer] Error 1
148 |
149 | ```
150 |
151 | Doh! The Provider doesn't know about `/users/:id`. On closer inspection, the provider only knows about `/user/:id` and `/users`.
152 |
153 | We need to have a conversation about what the endpoint should be, but first...
154 |
155 | *Move on to [step 3](//github.com/pact-foundation/pact-workshop-go/tree/step3)*
156 |
157 | ## Step 3 - Pact to the rescue
158 |
159 | Let's add Pact to the project. It comes in two parts.
160 |
161 | - Installing pact-go cli
162 | - Required to install pact-go system libraries
163 | - Adding pact-go as a dev dependency to your project.
164 |
165 | Always check the installation instructions in the [docs](https://github.com/pact-foundation/pact-go/tree/master?tab=readme-ov-file#installation) for your platform.
166 |
167 | The following command will install the pact-go CLI tool.
168 |
169 | ```console
170 | $ go install github.com/pact-foundation/pact-go/v2
171 | ```
172 |
173 | and we will use the pact-go CLI tool to install system libraries required by pact-go
174 |
175 | ```console
176 | $ pact-go -l DEBUG install
177 | ```
178 |
179 | You can use the provided make command to do this for you.
180 |
181 | ```console
182 | $ make install
183 | ```
184 |
185 | You can add `pact-go` to your project with the following
186 |
187 | ```console
188 | $ go get github.com/pact-foundation/pact-go/v2
189 | ```
190 |
191 | We can now write a consumer pact test for the `GET /users/:id` endpoint.
192 |
193 | Note how similar it looks to our unit test:
194 |
195 | *consumer/client/client_pact_test.go:*
196 |
197 | ```go
198 | t.Run("the user exists", func(t *testing.T) {
199 | id := 10
200 |
201 | err = mockProvider.
202 | AddInteraction().
203 | Given("User sally exists").
204 | UponReceiving("A request to login with user 'sally'").
205 | WithRequestPathMatcher("GET", Regex("/users/"+strconv.Itoa(id), "/users/[0-9]+")).
206 | WillRespondWith(200, func(b *consumer.V2ResponseBuilder) {
207 | b.BodyMatch(model.User{}).
208 | Header("Content-Type", Term("application/json", `application\/json`)).
209 | Header("X-Api-Correlation-Id", Like("100"))
210 | }).
211 | ExecuteTest(t, func(config consumer.MockServerConfig) error {
212 | // Act: test our API client behaves correctly
213 |
214 | // Get the Pact mock server URL
215 | u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))
216 |
217 | // Initialise the API client and point it at the Pact mock server
218 | client = &Client{
219 | BaseURL: u,
220 | }
221 |
222 | // // Execute the API client
223 | user, err := client.GetUser(id)
224 |
225 | // // Assert basic fact
226 | if user.ID != id {
227 | return fmt.Errorf("wanted user with ID %d but got %d", id, user.ID)
228 | }
229 |
230 | return err
231 | })
232 |
233 | assert.NoError(t, err)
234 |
235 | })
236 | ```
237 |
238 |
239 | 
240 |
241 |
242 | This test starts a Pact mock server on a random port that acts as our provider service. . We can access the update the `config.Host` & `config.Port` from `consumer.MockServerConfig` in the `ExecuteTest` block and pass these into the `Client` that we create, after initialising Pact. Pact will ensure our client makes the request stated in the interaction.
243 |
244 | Running this test still passes, but it creates a pact file which we can use to validate our assumptions on the provider side, and have conversation around.
245 |
246 | ```console
247 | $ make consumer
248 | ```
249 |
250 | A pact file should have been generated in *pacts/GoAdminService-GoUserService.json*
251 |
252 | *Move on to [step 4](//github.com/pact-foundation/pact-workshop-go/tree/step4)*
253 |
254 | ## Step 4 - Verify the provider
255 |
256 | 
257 |
258 | We now need to validate the pact generated by the consumer is valid, by executing it against the running service provider, which should fail:
259 |
260 | ```console
261 | $ make provider
262 |
263 | --- 🔨Running Provider Pact tests
264 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
265 | === RUN TestPactProvider
266 | 2024/09/04 17:57:08 API starting: port 52668 ([::]:52668)
267 | 2024-09-04T16:57:08.543176Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
268 | 2024-09-04T16:57:08.543378Z WARN ThreadId(11) pact_verifier::callback_executors: State Change ignored as there is no state change URL provided for interaction
269 | 2024-09-04T16:57:08.543404Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
270 | 2024-09-04T16:57:08.543486Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:52671/
271 | 2024-09-04T16:57:08.543488Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /users/10, query: None, headers: None, body: Missing )
272 | 2024-09-04T16:57:08.546904Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"content-length": ["111"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 16:57:08 GMT"], "x-api-correlation-id": ["0d3aaf0f-027f-4170-b77b-e5a0b11b7f6c"]}), body: Present(111 bytes, application/json) )
273 | 2024-09-04T16:57:08.549372Z WARN ThreadId(11) pact_matching::metrics:
274 |
275 | Please note:
276 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
277 |
278 |
279 |
280 | Verifying a pact between GoAdminService and GoUserService
281 |
282 | A request to login with user 'sally' (2ms loading, 167ms verification)
283 | Given User sally exists
284 | returns a response which
285 | has status code 200 (OK)
286 | includes headers
287 | "X-Api-Correlation-Id" with value "100" (OK)
288 | "Content-Type" with value "application/json" (OK)
289 | has a matching body (FAILED)
290 |
291 |
292 | Failures:
293 |
294 | 1) Verifying a pact between GoAdminService and GoUserService Given User sally exists - A request to login with user 'sally'
295 | 1.1) has a matching body
296 | $ -> Type mismatch: Expected [{"firstName":"Jean-Marie","id":10,"lastName":"de La Beaujardière😀😍","type":"admin","username":"sally"}] (Array) to be the same type as {"firstName":"Sally","id":10,"lastName":"McSmiley Face😀😍","type":"admin","username":"sally"} (Object)
297 |
298 | There were 1 pact failures
299 |
300 | === RUN TestPactProvider/Provider_pact_verification
301 | verifier.go:183: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
302 | === NAME TestPactProvider
303 | user_service_test.go:36: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
304 | --- FAIL: TestPactProvider (0.49s)
305 | --- FAIL: TestPactProvider/Provider_pact_verification (0.00s)
306 | FAIL
307 | FAIL github.com/pact-foundation/pact-workshop-go/provider 0.539s
308 | FAIL
309 | make: *** [provider] Error 1
310 | ```
311 |
312 | The test has failed, as the expected path `/users/:id` is actually triggering the `/users` endpoint (which we don't need), and returning a _list_ of Users instead of a _single_ User. We incorrectly believed our provider was following a RESTful design, but the authors were too lazy to implement a better routing solution 🤷🏻♂️.
313 |
314 | The correct endpoint should be `/user/:id`.
315 |
316 | Move on to [step 5](//github.com/pact-foundation/pact-workshop-go/tree/step5)*
317 |
318 | ## Step 5 - Back to the client we go
319 |
320 | 
321 |
322 | Let's update the consumer test and client to hit the correct path, and run the provider verification also:
323 |
324 | ```
325 | --- 🔨Running Consumer Pact tests
326 | go test -tags=integration -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientPact' -v
327 | === RUN TestClientPact_GetUser
328 | === RUN TestClientPact_GetUser/the_user_exists
329 | 2024-09-04T17:06:51.261564Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request GET /user/10
330 | 2024-09-04T17:06:51.262470Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
331 | 2024-09-04T17:06:51.263240Z INFO ThreadId(02) pact_mock_server::mock_server: Writing pact out to '/Users/yousaf.nabi/dev/pact-foundation/pact-workshop-go/pacts/GoAdminService-GoUserService.json'
332 | 2024/09/04 18:06:51 [ERROR] failed to log to stdout: can't set logger (applying the logger failed, perhaps because one is applied already).
333 | --- PASS: TestClientPact_GetUser (0.02s)
334 | --- PASS: TestClientPact_GetUser/the_user_exists (0.02s)
335 | PASS
336 | ok github.com/pact-foundation/pact-workshop-go/consumer/client 0.071s
337 | ```
338 |
339 | ```
340 | --- 🔨Running Provider Pact tests
341 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
342 | === RUN TestPactProvider
343 | 2024/09/04 18:05:44 API starting: port 52781 ([::]:52781)
344 | 2024-09-04T17:05:45.133153Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
345 | 2024-09-04T17:05:45.133206Z WARN ThreadId(11) pact_verifier::callback_executors: State Change ignored as there is no state change URL provided for interaction
346 | 2024-09-04T17:05:45.133232Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
347 | 2024-09-04T17:05:45.133294Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:52784/
348 | 2024-09-04T17:05:45.133297Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
349 | 2024-09-04T17:05:45.136291Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"date": ["Wed, 04 Sep 2024 17:05:45 GMT"], "content-length": ["109"], "content-type": ["application/json"], "x-api-correlation-id": ["1c0fa3a9-ebb7-4c21-ab01-345b882d0dc4"]}), body: Present(109 bytes, application/json) )
350 | 2024-09-04T17:05:45.137119Z WARN ThreadId(11) pact_matching::metrics:
351 |
352 | Please note:
353 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
354 |
355 |
356 |
357 | Verifying a pact between GoAdminService and GoUserService
358 |
359 | A request to login with user 'sally' (0s loading, 166ms verification)
360 | Given User sally exists
361 | returns a response which
362 | has status code 200 (OK)
363 | includes headers
364 | "X-Api-Correlation-Id" with value "100" (OK)
365 | "Content-Type" with value "application/json" (OK)
366 | has a matching body (OK)
367 |
368 |
369 | === RUN TestPactProvider/Provider_pact_verification
370 | --- PASS: TestPactProvider (0.48s)
371 | --- PASS: TestPactProvider/Provider_pact_verification (0.00s)
372 | PASS
373 | ok github.com/pact-foundation/pact-workshop-go/provider 0.532s
374 | ```
375 |
376 | Yay - green ✅!
377 |
378 | Move on to [step 6](//github.com/pact-foundation/pact-workshop-go/tree/step6)*
379 |
380 | ## Step 6 - Missing Users
381 |
382 | We're now going to add another scenario - what happens when we make a call for a user that doesn't exist? We assume we'll get a `404`, because that is the obvious thing to do.
383 |
384 | Let's write a test for this scenario, and then generate an updated pact file.
385 |
386 | *consumer/client/client_pact_test.go*:
387 | ```go
388 | t.Run("the user does not exist", func(t *testing.T) {
389 | id := 10
390 |
391 | err = mockProvider.
392 | AddInteraction().
393 | Given("User sally does not exist").
394 | UponReceiving("A request to login with user 'sally'").
395 | WithRequestPathMatcher("GET", Regex("/user/"+strconv.Itoa(id), "/user/[0-9]+")).
396 | WillRespondWith(404, func(b *consumer.V2ResponseBuilder) {
397 | b.Header("Content-Type", Term("application/json", `application\/json`)).
398 | Header("X-Api-Correlation-Id", Like("100"))
399 | }).
400 | ExecuteTest(t, func(config consumer.MockServerConfig) error {
401 | // Act: test our API client behaves correctly
402 |
403 | // Get the Pact mock server URL
404 | u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))
405 |
406 | // Initialise the API client and point it at the Pact mock server
407 | client = &Client{
408 | BaseURL: u,
409 | }
410 |
411 | // // Execute the API client
412 | _, err := client.GetUser(id)
413 | assert.Equal(t, ErrNotFound, err)
414 | return nil
415 | })
416 | assert.NoError(t, err)
417 | })
418 | ```
419 |
420 | Notice that our new test looks almost identical to our previous test, and only differs on the expectations of the _response_ - the HTTP request expectations are exactly the same.
421 |
422 | ```
423 | $ make consumer
424 |
425 | --- 🔨Running Consumer Pact tests
426 | go test -tags=integration -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientPact' -v
427 | === RUN TestClientPact_GetUser
428 | === RUN TestClientPact_GetUser/the_user_exists
429 | 2024-09-04T17:16:13.099939Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request GET /user/10
430 | 2024-09-04T17:16:13.101062Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
431 | 2024-09-04T17:16:13.101942Z INFO ThreadId(02) pact_mock_server::mock_server: Writing pact out to '/Users/yousaf.nabi/dev/pact-foundation/pact-workshop-go/pacts/GoAdminService-GoUserService.json'
432 | 2024/09/04 18:16:13 [ERROR] failed to log to stdout: can't set logger (applying the logger failed, perhaps because one is applied already).
433 | === RUN TestClientPact_GetUser/the_user_does_not_exist
434 | 2024-09-04T17:16:13.104166Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request GET /user/10
435 | 2024-09-04T17:16:13.104236Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
436 | 2024-09-04T17:16:13.104504Z INFO ThreadId(03) pact_mock_server::mock_server: Writing pact out to '/Users/yousaf.nabi/dev/pact-foundation/pact-workshop-go/pacts/GoAdminService-GoUserService.json'
437 | 2024-09-04T17:16:13.104923Z WARN ThreadId(03) pact_models::pact: Note: Existing pact is an older specification version (V2), and will be upgraded
438 | 2024/09/04 18:16:13 [ERROR] failed to log to stdout: can't set logger (applying the logger failed, perhaps because one is applied already).
439 | --- PASS: TestClientPact_GetUser (0.03s)
440 | --- PASS: TestClientPact_GetUser/the_user_exists (0.02s)
441 | --- PASS: TestClientPact_GetUser/the_user_does_not_exist (0.00s)
442 | PASS
443 | ok github.com/pact-foundation/pact-workshop-go/consumer/client 0.495s
444 | ```
445 |
446 | What does our provider have to say about this new test:
447 |
448 | ```
449 | --- 🔨Running Provider Pact tests
450 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
451 | === RUN TestPactProvider
452 | 2024/09/04 18:16:16 API starting: port 52955 ([::]:52955)
453 | 2024-09-04T17:16:17.050635Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
454 | 2024-09-04T17:16:17.050689Z WARN ThreadId(11) pact_verifier::callback_executors: State Change ignored as there is no state change URL provided for interaction
455 | 2024-09-04T17:16:17.050715Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
456 | 2024-09-04T17:16:17.050805Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:52958/
457 | 2024-09-04T17:16:17.050812Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
458 | 2024-09-04T17:16:17.053140Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"content-length": ["109"], "x-api-correlation-id": ["ff1ec2f1-c33e-4eaa-a4cf-108680275e0f"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:16:17 GMT"]}), body: Present(109 bytes, application/json) )
459 | 2024-09-04T17:16:17.200990Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
460 | 2024-09-04T17:16:17.201005Z WARN ThreadId(11) pact_verifier::callback_executors: State Change ignored as there is no state change URL provided for interaction
461 | 2024-09-04T17:16:17.201014Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
462 | 2024-09-04T17:16:17.201045Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:52958/
463 | 2024-09-04T17:16:17.201047Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
464 | 2024-09-04T17:16:17.202673Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"content-length": ["109"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:16:17 GMT"], "x-api-correlation-id": ["14d99164-279d-4bba-b10e-93ac44d2113a"]}), body: Present(109 bytes, application/json) )
465 | 2024-09-04T17:16:17.203388Z WARN ThreadId(11) pact_matching::metrics:
466 |
467 | Please note:
468 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
469 |
470 |
471 |
472 | Verifying a pact between GoAdminService and GoUserService
473 |
474 | A request to login with user 'sally' (0s loading, 167ms verification)
475 | Given User sally does not exist
476 | returns a response which
477 | has status code 404 (FAILED)
478 | includes headers
479 | "X-Api-Correlation-Id" with value "100" (OK)
480 | "Content-Type" with value "application/json" (OK)
481 | has a matching body (OK)
482 |
483 | A request to login with user 'sally' (0s loading, 149ms verification)
484 | Given User sally exists
485 | returns a response which
486 | has status code 200 (OK)
487 | includes headers
488 | "Content-Type" with value "application/json" (OK)
489 | "X-Api-Correlation-Id" with value "100" (OK)
490 | has a matching body (OK)
491 |
492 |
493 | Failures:
494 |
495 | 1) Verifying a pact between GoAdminService and GoUserService Given User sally does not exist - A request to login with user 'sally'
496 | 1.1) has status code 404
497 | expected 404 but was 200
498 |
499 | There were 1 pact failures
500 |
501 | === RUN TestPactProvider/Provider_pact_verification
502 | verifier.go:183: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
503 | === NAME TestPactProvider
504 | user_service_test.go:36: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
505 | --- FAIL: TestPactProvider (0.65s)
506 | --- FAIL: TestPactProvider/Provider_pact_verification (0.00s)
507 | FAIL
508 | FAIL github.com/pact-foundation/pact-workshop-go/provider 0.698s
509 | FAIL
510 | make: *** [provider] Error 1
511 | ```
512 |
513 | We expected this failure, because the user we are requesting does in fact exist! What we want to test for, is what happens if there is a different _state_ on the Provider. This is what is referred to as "Provider states", and how Pact gets around test ordering and related issues.
514 |
515 | We could resolve this by updating our consumer test to use a known non-existent User, but it's worth understanding how Provider states work more generally.
516 |
517 | *Move on to [step 7](//github.com/pact-foundation/pact-workshop-go/tree/step7)*
518 |
519 | ## Step 7 - Update our API to handle missing users
520 |
521 | Our code already deals with missing users and sends a `404` response, however our test data fixture always has Sally (user `10`) in our database.
522 |
523 | In this step, we will add a state handler (`StateHandlers`) to our Pact tests, which will update the state of our data store depending on which states.
524 |
525 | States are invoked prior to the actual test function is invoked. You can see the full [lifecycle here](https://github.com/pact-foundation/pact-go#lifecycle-of-a-provider-verification).
526 |
527 | We're going to add handlers for our two states - when Sally does and does not exist.
528 |
529 | ```go
530 | var stateHandlers = models.StateHandlers{
531 | "User sally exists": func(setup bool, s models.ProviderState) (models.ProviderStateResponse, error) {
532 | userRepository = sallyExists
533 | return models.ProviderStateResponse{}, nil
534 | },
535 | "User sally does not exist": func(setup bool, s models.ProviderState) (models.ProviderStateResponse, error) {
536 | userRepository = sallyDoesNotExist
537 | return models.ProviderStateResponse{}, nil
538 | },
539 | }
540 | ```
541 |
542 | Let's see how we go now:
543 |
544 | ```
545 | $ make provider
546 |
547 | --- 🔨Running Provider Pact tests
548 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
549 | === RUN TestPactProvider
550 | 2024/09/04 18:23:27 API starting: port 53091 ([::]:53091)
551 | 2024-09-04T17:23:28.194956Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
552 | 2024/09/04 18:23:28 [INFO] executing state handler middleware
553 | 2024-09-04T17:23:28.340008Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
554 | 2024-09-04T17:23:28.340096Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53094/
555 | 2024-09-04T17:23:28.340101Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
556 | 2024-09-04T17:23:28.340969Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 404, headers: Some({"content-length": ["0"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:23:28 GMT"], "x-api-correlation-id": ["c958d383-d67b-4c95-b6a0-d48c77adf315"]}), body: Empty )
557 | 2024-09-04T17:23:28.341210Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
558 | 2024/09/04 18:23:28 [INFO] executing state handler middleware
559 | 2024-09-04T17:23:28.650479Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
560 | 2024/09/04 18:23:28 [INFO] executing state handler middleware
561 | 2024-09-04T17:23:28.804271Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
562 | 2024-09-04T17:23:28.804312Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53094/
563 | 2024-09-04T17:23:28.804318Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
564 | 2024-09-04T17:23:28.805289Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"content-type": ["application/json"], "content-length": ["109"], "date": ["Wed, 04 Sep 2024 17:23:28 GMT"], "x-api-correlation-id": ["5b41178d-db3e-495e-8bd5-2fd525e5eef6"]}), body: Present(109 bytes, application/json) )
565 | 2024-09-04T17:23:28.805902Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally exists' for 'A request to login with user 'sally''
566 | 2024/09/04 18:23:28 [INFO] executing state handler middleware
567 | 2024-09-04T17:23:28.964743Z WARN ThreadId(11) pact_matching::metrics:
568 |
569 | Please note:
570 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
571 |
572 |
573 |
574 | Verifying a pact between GoAdminService and GoUserService
575 |
576 | A request to login with user 'sally' (0s loading, 482ms verification)
577 | Given User sally does not exist
578 | returns a response which
579 | has status code 404 (OK)
580 | includes headers
581 | "X-Api-Correlation-Id" with value "100" (OK)
582 | "Content-Type" with value "application/json" (OK)
583 | has a matching body (OK)
584 |
585 | A request to login with user 'sally' (0s loading, 472ms verification)
586 | Given User sally exists
587 | returns a response which
588 | has status code 200 (OK)
589 | includes headers
590 | "X-Api-Correlation-Id" with value "100" (OK)
591 | "Content-Type" with value "application/json" (OK)
592 | has a matching body (OK)
593 |
594 |
595 | === RUN TestPactProvider/Provider_pact_verification
596 | --- PASS: TestPactProvider (1.28s)
597 | --- PASS: TestPactProvider/Provider_pact_verification (0.00s)
598 | PASS
599 | ok github.com/pact-foundation/pact-workshop-go/provider 1.883s
600 | ```
601 |
602 | *Move on to [step 8](//github.com/pact-foundation/pact-workshop-go/tree/step8)*
603 |
604 | ## Step 8 - Authorization
605 |
606 | It turns out that not everyone should be able to use the API. After a discussion with the team, it was decided that a time-bound bearer token would suffice.
607 |
608 | In the case a valid bearer token is not provided, we expect a `401`. Let's update the consumer test cases to pass the bearer token, and capture this new `401` scenario.
609 |
610 | ```
611 | $ make consumer
612 |
613 | --- 🔨Running Consumer Pact tests
614 | go test -tags=integration -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientPact' -v
615 | === RUN TestClientPact_GetUser
616 | === RUN TestClientPact_GetUser/the_user_exists
617 | 2024-09-04T17:26:31.517409Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request GET /user/10
618 | 2024-09-04T17:26:31.518833Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
619 | 2024-09-04T17:26:31.520040Z INFO ThreadId(02) pact_mock_server::mock_server: Writing pact out to '/Users/yousaf.nabi/dev/pact-foundation/pact-workshop-go/pacts/GoAdminService-GoUserService.json'
620 | 2024/09/04 18:26:31 [ERROR] failed to log to stdout: can't set logger (applying the logger failed, perhaps because one is applied already).
621 | === RUN TestClientPact_GetUser/the_user_does_not_exist
622 | 2024-09-04T17:26:31.522280Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request GET /user/10
623 | 2024-09-04T17:26:31.522377Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
624 | 2024-09-04T17:26:31.522601Z INFO ThreadId(03) pact_mock_server::mock_server: Writing pact out to '/Users/yousaf.nabi/dev/pact-foundation/pact-workshop-go/pacts/GoAdminService-GoUserService.json'
625 | 2024-09-04T17:26:31.522966Z WARN ThreadId(03) pact_models::pact: Note: Existing pact is an older specification version (V2), and will be upgraded
626 | 2024/09/04 18:26:31 [ERROR] failed to log to stdout: can't set logger (applying the logger failed, perhaps because one is applied already).
627 | === RUN TestClientPact_GetUser/not_authenticated
628 | 2024-09-04T17:26:31.524071Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request GET /user/10
629 | 2024-09-04T17:26:31.524138Z INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response
630 | 2024-09-04T17:26:31.524262Z INFO ThreadId(02) pact_mock_server::mock_server: Writing pact out to '/Users/yousaf.nabi/dev/pact-foundation/pact-workshop-go/pacts/GoAdminService-GoUserService.json'
631 | 2024-09-04T17:26:31.524448Z WARN ThreadId(02) pact_models::pact: Note: Existing pact is an older specification version (V2), and will be upgraded
632 | 2024/09/04 18:26:31 [ERROR] failed to log to stdout: can't set logger (applying the logger failed, perhaps because one is applied already).
633 | --- PASS: TestClientPact_GetUser (0.03s)
634 | --- PASS: TestClientPact_GetUser/the_user_exists (0.02s)
635 | --- PASS: TestClientPact_GetUser/the_user_does_not_exist (0.00s)
636 | --- PASS: TestClientPact_GetUser/not_authenticated (0.00s)
637 | PASS
638 | ok github.com/pact-foundation/pact-workshop-go/consumer/client 0.473s
639 | ```
640 |
641 | We should now have two interactions in our pact file.
642 |
643 | Our verification now fails, as our consumer is sending a Bearer token that is not yet understood by our provider.
644 |
645 | ```
646 | $ make provider
647 |
648 | --- 🔨Running Provider Pact tests
649 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
650 | === RUN TestPactProvider
651 | 2024/09/04 18:26:33 API starting: port 53171 ([::]:53171)
652 | 2024-09-04T17:26:33.691144Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
653 | 2024/09/04 18:26:33 [INFO] executing state handler middleware
654 | 2024/09/04 18:26:33 [WARN] no state handler found for state: User is not authenticated
655 | 2024-09-04T17:26:33.862810Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
656 | 2024-09-04T17:26:33.862914Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53174/
657 | 2024-09-04T17:26:33.862919Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
658 | 2024-09-04T17:26:33.863928Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"x-api-correlation-id": ["df1e249c-ef86-4951-89cc-b36a865406b9"], "content-length": ["109"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:26:33 GMT"]}), body: Present(109 bytes, application/json) )
659 | 2024-09-04T17:26:33.864168Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
660 | 2024/09/04 18:26:34 [INFO] executing state handler middleware
661 | 2024/09/04 18:26:34 [WARN] no state handler found for state: User is not authenticated
662 | 2024-09-04T17:26:34.166077Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
663 | 2024/09/04 18:26:34 [INFO] executing state handler middleware
664 | 2024-09-04T17:26:34.321688Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
665 | 2024-09-04T17:26:34.321728Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53174/
666 | 2024-09-04T17:26:34.321731Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
667 | 2024-09-04T17:26:34.322490Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 404, headers: Some({"content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:26:34 GMT"], "content-length": ["0"], "x-api-correlation-id": ["4f721ba2-d17f-4111-8652-6176f03db0c9"]}), body: Empty )
668 | 2024-09-04T17:26:34.322607Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
669 | 2024/09/04 18:26:34 [INFO] executing state handler middleware
670 | 2024-09-04T17:26:34.643400Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
671 | 2024/09/04 18:26:34 [INFO] executing state handler middleware
672 | 2024-09-04T17:26:34.848872Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
673 | 2024-09-04T17:26:34.848925Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53174/
674 | 2024-09-04T17:26:34.848928Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
675 | 2024-09-04T17:26:34.849747Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"content-type": ["application/json"], "content-length": ["109"], "date": ["Wed, 04 Sep 2024 17:26:34 GMT"], "x-api-correlation-id": ["56978492-00c2-4f75-9697-d885caa660a0"]}), body: Present(109 bytes, application/json) )
676 | 2024-09-04T17:26:34.850493Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally exists' for 'A request to login with user 'sally''
677 | 2024/09/04 18:26:35 [INFO] executing state handler middleware
678 | 2024-09-04T17:26:35.004331Z WARN ThreadId(11) pact_matching::metrics:
679 |
680 | Please note:
681 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
682 |
683 |
684 |
685 | Verifying a pact between GoAdminService and GoUserService
686 |
687 | A request to login with user 'sally' (0s loading, 498ms verification)
688 | Given User is not authenticated
689 | returns a response which
690 | has status code 401 (FAILED)
691 | includes headers
692 | "X-Api-Correlation-Id" with value "100" (OK)
693 | "Content-Type" with value "application/json" (OK)
694 | has a matching body (OK)
695 |
696 | A request to login with user 'sally' (0s loading, 478ms verification)
697 | Given User sally does not exist
698 | returns a response which
699 | has status code 404 (OK)
700 | includes headers
701 | "X-Api-Correlation-Id" with value "100" (OK)
702 | "Content-Type" with value "application/json" (OK)
703 | has a matching body (OK)
704 |
705 | A request to login with user 'sally' (0s loading, 511ms verification)
706 | Given User sally exists
707 | returns a response which
708 | has status code 200 (OK)
709 | includes headers
710 | "X-Api-Correlation-Id" with value "100" (OK)
711 | "Content-Type" with value "application/json" (OK)
712 | has a matching body (OK)
713 |
714 |
715 | Failures:
716 |
717 | 1) Verifying a pact between GoAdminService and GoUserService Given User is not authenticated - A request to login with user 'sally'
718 | 1.1) has status code 401
719 | expected 401 but was 200
720 |
721 | There were 1 pact failures
722 |
723 | === RUN TestPactProvider/Provider_pact_verification
724 | verifier.go:183: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
725 | === NAME TestPactProvider
726 | user_service_test.go:44: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
727 | --- FAIL: TestPactProvider (1.80s)
728 | --- FAIL: TestPactProvider/Provider_pact_verification (0.00s)
729 | FAIL
730 | FAIL github.com/pact-foundation/pact-workshop-go/provider 2.291s
731 | FAIL
732 | make: *** [provider] Error 1
733 | ```
734 |
735 | *Move on to [step 9](//github.com/pact-foundation/pact-workshop-go/tree/step9)*
736 |
737 | ## Step 9 - Implement authorisation on the provider
738 |
739 | Like most tokens, our bearer token is going to be dependent on the date/time it was generated. For the purposes of our API, it's rather crude:
740 |
741 | ```go
742 | func getAuthToken() string {
743 | return fmt.Sprintf("Bearer %s", time.Now().Format("2006-01-02T15:04"))
744 | }
745 | ```
746 |
747 | This means that a client must present an HTTP `Authorization` header that looks as follows:
748 |
749 | ```
750 | Authorization: Bearer 2006-01-02T15:04
751 | ```
752 |
753 | We have created a small middleware to wrap our functions and return a `401`:
754 |
755 | ```go
756 | func IsAuthenticated(h http.HandlerFunc) http.HandlerFunc {
757 | return func(w http.ResponseWriter, r *http.Request) {
758 | if r.Header.Get("Authorization") == getAuthToken() {
759 | h.ServeHTTP(w, r)
760 | } else {
761 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
762 | w.WriteHeader(http.StatusUnauthorized)
763 | }
764 | }
765 | }
766 | ```
767 |
768 | Let's test this out:
769 |
770 | ```
771 | $ make provider
772 |
773 | --- 🔨Running Provider Pact tests
774 | --- 🔨Running Provider Pact tests
775 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
776 | === RUN TestPactProvider
777 | 2024/09/04 18:33:24 API starting: port 53320 ([::]:53320)
778 | 2024-09-04T17:33:24.470513Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
779 | 2024/09/04 18:33:24 [INFO] executing state handler middleware
780 | 2024/09/04 18:33:24 [WARN] no state handler found for state: User is not authenticated
781 | 2024-09-04T17:33:24.628543Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
782 | 2024-09-04T17:33:24.628943Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53323/
783 | 2024-09-04T17:33:24.628947Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
784 | 2024-09-04T17:33:24.629996Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 401, headers: Some({"content-type": ["application/json"], "content-length": ["0"], "date": ["Wed, 04 Sep 2024 17:33:24 GMT"], "x-api-correlation-id": ["412a28a3-b08f-4fce-b97d-65a9f90fe180"]}), body: Empty )
785 | 2024-09-04T17:33:24.630616Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
786 | 2024/09/04 18:33:24 [INFO] executing state handler middleware
787 | 2024/09/04 18:33:24 [WARN] no state handler found for state: User is not authenticated
788 | 2024-09-04T17:33:24.932360Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
789 | 2024/09/04 18:33:25 [INFO] executing state handler middleware
790 | 2024-09-04T17:33:25.085895Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
791 | 2024-09-04T17:33:25.085991Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53323/
792 | 2024-09-04T17:33:25.085999Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
793 | 2024-09-04T17:33:25.086912Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 401, headers: Some({"content-length": ["0"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:33:25 GMT"], "x-api-correlation-id": ["0fab9b01-3710-49a7-9a57-d826cc79ee4f"]}), body: Empty )
794 | 2024-09-04T17:33:25.087072Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
795 | 2024/09/04 18:33:25 [INFO] executing state handler middleware
796 | 2024-09-04T17:33:25.387705Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
797 | 2024/09/04 18:33:25 [INFO] executing state handler middleware
798 | 2024-09-04T17:33:25.535975Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
799 | 2024-09-04T17:33:25.536019Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53323/
800 | 2024-09-04T17:33:25.536021Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
801 | 2024-09-04T17:33:25.536676Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 401, headers: Some({"date": ["Wed, 04 Sep 2024 17:33:25 GMT"], "content-length": ["0"], "content-type": ["application/json"], "x-api-correlation-id": ["e6e7fc30-7fed-4b75-9294-c189890c7443"]}), body: Empty )
802 | 2024-09-04T17:33:25.536793Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally exists' for 'A request to login with user 'sally''
803 | 2024/09/04 18:33:25 [INFO] executing state handler middleware
804 | 2024-09-04T17:33:25.690099Z WARN ThreadId(11) pact_matching::metrics:
805 |
806 | Please note:
807 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
808 |
809 |
810 |
811 | Verifying a pact between GoAdminService and GoUserService
812 |
813 | A request to login with user 'sally' (0s loading, 492ms verification)
814 | Given User is not authenticated
815 | returns a response which
816 | has status code 401 (OK)
817 | includes headers
818 | "X-Api-Correlation-Id" with value "100" (OK)
819 | "Content-Type" with value "application/json" (OK)
820 | has a matching body (OK)
821 |
822 | A request to login with user 'sally' (0s loading, 454ms verification)
823 | Given User sally does not exist
824 | returns a response which
825 | has status code 404 (FAILED)
826 | includes headers
827 | "Content-Type" with value "application/json" (OK)
828 | "X-Api-Correlation-Id" with value "100" (OK)
829 | has a matching body (OK)
830 |
831 | A request to login with user 'sally' (0s loading, 449ms verification)
832 | Given User sally exists
833 | returns a response which
834 | has status code 200 (FAILED)
835 | includes headers
836 | "X-Api-Correlation-Id" with value "100" (OK)
837 | "Content-Type" with value "application/json" (OK)
838 | has a matching body (FAILED)
839 |
840 |
841 | Failures:
842 |
843 | 1) Verifying a pact between GoAdminService and GoUserService Given User sally does not exist - A request to login with user 'sally'
844 | 1.1) has status code 404
845 | expected 404 but was 401
846 | 2) Verifying a pact between GoAdminService and GoUserService Given User sally exists - A request to login with user 'sally'
847 | 2.1) has a matching body
848 | / -> Expected body Present(98 bytes) but was empty
849 | 2.2) has status code 200
850 | expected 200 but was 401
851 |
852 | There were 2 pact failures
853 |
854 | === RUN TestPactProvider/Provider_pact_verification
855 | verifier.go:183: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
856 | === NAME TestPactProvider
857 | user_service_test.go:44: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
858 | --- FAIL: TestPactProvider (1.74s)
859 | --- FAIL: TestPactProvider/Provider_pact_verification (0.00s)
860 | FAIL
861 | FAIL github.com/pact-foundation/pact-workshop-go/provider 2.247s
862 | FAIL
863 | make: *** [provider] Error 1
864 | ```
865 |
866 | Oh, dear. _Both_ tests are now failing. Can you understand why?
867 |
868 | *Move on to [step 10](//github.com/pact-foundation/pact-workshop-go/tree/step10)*
869 |
870 | ## Step 10 - Request Filters on the Provider
871 |
872 | Because our pact file has static data in it, our bearer token is now out of date, so when Pact verification passes it to the Provider we get a `401`. There are multiple ways to resolve this - mocking or stubbing out the authentication component is a common one. In our use case, we are going to use a process referred to as _Request Filtering_, using a `RequestFilter`.
873 |
874 | _NOTE_: This is an advanced concept and should be used carefully, as it has the potential to invalidate a contract by bypassing its constraints. See https://github.com/pact-foundation/pact-go#request-filtering for more details on this.
875 |
876 | The approach we are going to take to inject the header is as follows:
877 |
878 | 1. If we receive any Authorization header, we override the incoming request with a valid (in time) Authorization header, and continue with whatever call was being made
879 | 1. If we don't recieve a header, we do nothing
880 |
881 | _NOTE_: We are not considering the `403` scenario in this example.
882 |
883 | Here is the request filter:
884 |
885 | ```go
886 | // Simulates the need to set a time-bound authorization token,
887 | // such as an OAuth bearer token
888 | func fixBearerToken(next http.Handler) http.Handler {
889 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
890 | // Only set the correct bearer token, if one was provided in the first place
891 | if r.Header.Get("Authorization") != "" {
892 | r.Header.Set("Authorization", getAuthToken())
893 | }
894 | next.ServeHTTP(w, r)
895 | })
896 | }
897 | ```
898 |
899 | We can now run the Provider tests
900 |
901 | ```
902 | $ make provider
903 |
904 | --- 🔨Running Provider Pact tests
905 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
906 | === RUN TestPactProvider
907 | 2024/09/04 18:41:09 API starting: port 53466 ([::]:53466)
908 | 2024-09-04T17:41:10.191761Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
909 | 2024/09/04 18:41:10 [INFO] executing state handler middleware
910 | 2024/09/04 18:41:10 [WARN] no state handler found for state: User is not authenticated
911 | 2024-09-04T17:41:10.339448Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
912 | 2024-09-04T17:41:10.339564Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53469/
913 | 2024-09-04T17:41:10.339568Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
914 | 2024-09-04T17:41:10.340463Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 401, headers: Some({"x-api-correlation-id": ["bc381d46-18e7-46ef-9e15-66fbf5a37de2"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:41:10 GMT"], "content-length": ["0"]}), body: Empty )
915 | 2024-09-04T17:41:10.342522Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
916 | 2024/09/04 18:41:10 [INFO] executing state handler middleware
917 | 2024/09/04 18:41:10 [WARN] no state handler found for state: User is not authenticated
918 | 2024-09-04T17:41:10.627069Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
919 | 2024/09/04 18:41:10 [INFO] executing state handler middleware
920 | 2024-09-04T17:41:10.779030Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
921 | 2024-09-04T17:41:10.779077Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53469/
922 | 2024-09-04T17:41:10.779081Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
923 | 2024-09-04T17:41:10.779928Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 404, headers: Some({"date": ["Wed, 04 Sep 2024 17:41:10 GMT"], "content-type": ["application/json"], "content-length": ["0"], "x-api-correlation-id": ["3b14465b-c616-4497-a8f4-97249514ebc4"]}), body: Empty )
924 | 2024-09-04T17:41:10.780045Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
925 | 2024/09/04 18:41:10 [INFO] executing state handler middleware
926 | 2024-09-04T17:41:11.085005Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
927 | 2024/09/04 18:41:11 [INFO] executing state handler middleware
928 | 2024-09-04T17:41:11.234108Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
929 | 2024-09-04T17:41:11.234150Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53469/
930 | 2024-09-04T17:41:11.234153Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
931 | 2024-09-04T17:41:11.235086Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"content-length": ["109"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:41:11 GMT"], "x-api-correlation-id": ["3c49756c-2f81-400c-a84c-0ff889fe2b9f"]}), body: Present(109 bytes, application/json) )
932 | 2024-09-04T17:41:11.235827Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally exists' for 'A request to login with user 'sally''
933 | 2024/09/04 18:41:11 [INFO] executing state handler middleware
934 | 2024-09-04T17:41:11.382223Z WARN ThreadId(11) pact_matching::metrics:
935 |
936 | Please note:
937 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
938 |
939 |
940 |
941 | Verifying a pact between GoAdminService and GoUserService
942 |
943 | A request to login with user 'sally' (2ms loading, 469ms verification)
944 | Given User is not authenticated
945 | returns a response which
946 | has status code 401 (OK)
947 | includes headers
948 | "X-Api-Correlation-Id" with value "100" (OK)
949 | "Content-Type" with value "application/json" (OK)
950 | has a matching body (OK)
951 |
952 | A request to login with user 'sally' (2ms loading, 454ms verification)
953 | Given User sally does not exist
954 | returns a response which
955 | has status code 404 (OK)
956 | includes headers
957 | "X-Api-Correlation-Id" with value "100" (OK)
958 | "Content-Type" with value "application/json" (OK)
959 | has a matching body (OK)
960 |
961 | A request to login with user 'sally' (2ms loading, 443ms verification)
962 | Given User sally exists
963 | returns a response which
964 | has status code 200 (OK)
965 | includes headers
966 | "Content-Type" with value "application/json" (OK)
967 | "X-Api-Correlation-Id" with value "100" (OK)
968 | has a matching body (OK)
969 |
970 |
971 | === RUN TestPactProvider/Provider_pact_verification
972 | --- PASS: TestPactProvider (1.70s)
973 | --- PASS: TestPactProvider/Provider_pact_verification (0.00s)
974 | PASS
975 | ok github.com/pact-foundation/pact-workshop-go/provider 2.146s
976 | ```
977 |
978 | *Move on to [step 11](//github.com/pact-foundation/pact-workshop-go/tree/step11)*
979 |
980 | ## Step 11 - Using a Pact Broker
981 |
982 | 
983 |
984 | We've been publishing our pacts from the consumer project by essentially sharing the file system with the provider. But this is not very manageable when you have multiple teams contributing to the code base, and pushing to CI. We can use a [Pact Broker](https://pactflow.io) to do this instead.
985 |
986 | Using a broker simplifies the management of pacts and adds a number of useful features, including some safety enhancements for continuous delivery which we'll see shortly.
987 |
988 | In this workshop we will be using the open source Pact broker.
989 |
990 | ### Running the Pact Broker with docker compose
991 |
992 | In the root directory, run:
993 |
994 | ```console
995 | docker compose up
996 | ```
997 |
998 |
999 | ### Publish from consumer
1000 |
1001 | First, in the consumer project we need to tell Pact about our broker.
1002 |
1003 | We will need to install the pact broker cli tool, which is available via a few different mechanisms, see the installation docs. https://github.com/pact-foundation/pact_broker-client?tab=readme-ov-file#installation
1004 |
1005 | ```console
1006 | $ make install_cli
1007 | ```
1008 |
1009 | This will make the `pact-broker` application available to use in `pact/bin`
1010 |
1011 | ```console
1012 | $ make publish
1013 |
1014 | --- 📝 Publishing Pacts
1015 | pact/bin/pact-broker publish /Users/yousaf.nabi/dev/pact-foundation/pact-workshop-go/pacts --consumer-app-version 3da6d1932e0c9a031f6f6998255d3b2ee9bdda9c --branch step11 \
1016 | -b http://localhost:8081 -u pact_workshop -p pact_workshop
1017 | Created GoAdminService version 3da6d1932e0c9a031f6f6998255d3b2ee9bdda9c with branch step11
1018 | Pact successfully published for GoAdminService version 3da6d1932e0c9a031f6f6998255d3b2ee9bdda9c and provider GoUserService.
1019 | View the published pact at http://localhost:8081/pacts/provider/GoUserService/consumer/GoAdminService/version/3da6d1932e0c9a031f6f6998255d3b2ee9bdda9c
1020 | Events detected: contract_published, contract_content_changed (first time untagged pact published)
1021 | Next steps:
1022 | * Add Pact verification tests to the GoUserService build. See https://docs.pact.io/go/provider_verification
1023 | * Configure separate GoUserService pact verification build and webhook to trigger it when the pact content changes. See https://docs.pact.io/go/webhooks
1024 |
1025 | Pact contract publishing complete!
1026 |
1027 | Head over to http://localhost:8081 and login with pact_workshop/pact_workshop
1028 | to see your published contracts.
1029 | ```
1030 |
1031 | Have a browse around the broker and see your newly published contract!
1032 |
1033 | ### Provider
1034 |
1035 | All we need to do for the provider is update where it finds its pacts, from local URLs, to one from a broker.
1036 |
1037 | ```go
1038 | _, err := pact.VerifyProvider(t, types.VerifyRequest{
1039 | ProviderBaseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
1040 | Branch: "master",
1041 | FailIfNoPactsFound: false,
1042 | Verbose: false,
1043 | // Use this if you want to test without the Pact Broker
1044 | // PactFiles: []string{filepath.FromSlash(fmt.Sprintf("%s/GoAdminService-GoUserService.json", os.Getenv("PACT_DIR")))},
1045 | BrokerURL: fmt.Sprintf("%s://%s", os.Getenv("PACT_BROKER_PROTO"), os.Getenv("PACT_BROKER_URL")),
1046 | BrokerUsername: os.Getenv("PACT_BROKER_USERNAME"),
1047 | BrokerPassword: os.Getenv("PACT_BROKER_PASSWORD"),
1048 | PublishVerificationResults: true,
1049 | ProviderVersion: os.Getenv("VERSION_COMMIT"),
1050 | StateHandlers: stateHandlers,
1051 | RequestFilter: fixBearerToken,
1052 | BeforeEach: func() error {
1053 | userRepository = sallyExists
1054 | return nil
1055 | },
1056 | })
1057 | ```
1058 |
1059 | Let's run the provider verification one last time after this change:
1060 |
1061 | ```
1062 | $ make provider
1063 |
1064 | --- 🔨Running Provider Pact tests
1065 | go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v
1066 | === RUN TestPactProvider
1067 | 2024/09/04 18:50:21 API starting: port 53692 ([::]:53692)
1068 | 2024-09-04T17:50:22.071802Z INFO ThreadId(11) pact_verifier::pact_broker: Fetching path '' from pact broker
1069 | 2024-09-04T17:50:22.091747Z INFO ThreadId(11) pact_verifier::pact_broker: Fetching path '/pacts/provider/GoUserService/for-verification' from pact broker
1070 | 2024-09-04T17:50:22.145165Z INFO ThreadId(11) pact_verifier::pact_broker: Fetching path '/pacts/provider/GoUserService/consumer/GoAdminService/pact-version/559153482081f71d7cfb2e86cb07a021e3079f8b/metadata/c1tdW2xdPXRydWUmc1tdW2N2XT05' from pact broker
1071 | 2024-09-04T17:50:22.302495Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
1072 | 2024/09/04 18:50:22 [INFO] executing state handler middleware
1073 | 2024/09/04 18:50:22 [WARN] no state handler found for state: User is not authenticated
1074 | 2024-09-04T17:50:22.466578Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
1075 | 2024-09-04T17:50:22.467533Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53695/
1076 | 2024-09-04T17:50:22.467538Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: None, body: Missing )
1077 | 2024-09-04T17:50:22.468433Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 401, headers: Some({"x-api-correlation-id": ["38a276d3-19fa-4cf5-9be2-be837649307c"], "content-length": ["0"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:50:22 GMT"]}), body: Empty )
1078 | 2024-09-04T17:50:22.469670Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User is not authenticated' for 'A request to login with user 'sally''
1079 | 2024/09/04 18:50:22 [INFO] executing state handler middleware
1080 | 2024/09/04 18:50:22 [WARN] no state handler found for state: User is not authenticated
1081 | 2024-09-04T17:50:22.807828Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
1082 | 2024/09/04 18:50:22 [INFO] executing state handler middleware
1083 | 2024-09-04T17:50:22.964920Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
1084 | 2024-09-04T17:50:22.964960Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53695/
1085 | 2024-09-04T17:50:22.964963Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
1086 | 2024-09-04T17:50:22.965511Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 404, headers: Some({"x-api-correlation-id": ["c0e800d2-0881-4863-a6b7-953211cc796a"], "date": ["Wed, 04 Sep 2024 17:50:22 GMT"], "content-type": ["application/json"], "content-length": ["0"]}), body: Empty )
1087 | 2024-09-04T17:50:22.965594Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally does not exist' for 'A request to login with user 'sally''
1088 | 2024/09/04 18:50:23 [INFO] executing state handler middleware
1089 | 2024-09-04T17:50:23.289727Z INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'User sally exists' for 'A request to login with user 'sally''
1090 | 2024/09/04 18:50:23 [INFO] executing state handler middleware
1091 | 2024-09-04T17:50:23.445333Z INFO ThreadId(11) pact_verifier: Running provider verification for 'A request to login with user 'sally''
1092 | 2024-09-04T17:50:23.445375Z INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:53695/
1093 | 2024-09-04T17:50:23.445377Z INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /user/10, query: None, headers: Some({"Authorization": ["Bearer 2019-01-01"]}), body: Missing )
1094 | 2024-09-04T17:50:23.446165Z INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"x-api-correlation-id": ["73d018c7-5967-464c-a3a8-54b6e55ede2a"], "content-length": ["109"], "content-type": ["application/json"], "date": ["Wed, 04 Sep 2024 17:50:23 GMT"]}), body: Present(109 bytes, application/json) )
1095 | 2024-09-04T17:50:23.447833Z INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'User sally exists' for 'A request to login with user 'sally''
1096 | 2024/09/04 18:50:23 [INFO] executing state handler middleware
1097 | 2024-09-04T17:50:23.602399Z INFO ThreadId(11) pact_verifier: Publishing verification results back to the Pact Broker
1098 | 2024-09-04T17:50:23.755123Z INFO ThreadId(11) pact_verifier::pact_broker: Fetching path '/pacticipants/GoUserService' from pact broker
1099 | 2024-09-04T17:50:23.812391Z INFO ThreadId(11) pact_verifier: Results published to Pact Broker
1100 | 2024-09-04T17:50:23.812477Z WARN ThreadId(11) pact_matching::metrics:
1101 |
1102 | Please note:
1103 | We are tracking events anonymously to gather important usage statistics like Pact version and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment variable to 'true'.
1104 |
1105 |
1106 | The pact at http://localhost:8081/pacts/provider/GoUserService/consumer/GoAdminService/pact-version/559153482081f71d7cfb2e86cb07a021e3079f8b is being verified because the pact content belongs to the consumer version matching the following criterion:
1107 | * latest version of GoAdminService that has a pact with GoUserService (3da6d1932e0c9a031f6f6998255d3b2ee9bdda9c)
1108 |
1109 | Verifying a pact between GoAdminService and GoUserService
1110 |
1111 | A request to login with user 'sally' (250ms loading, 493ms verification)
1112 | Given User is not authenticated
1113 | returns a response which
1114 | has status code 401 (OK)
1115 | includes headers
1116 | "X-Api-Correlation-Id" with value "100" (OK)
1117 | "Content-Type" with value "application/json" (OK)
1118 | has a matching body (OK)
1119 |
1120 | A request to login with user 'sally' (250ms loading, 489ms verification)
1121 | Given User sally does not exist
1122 | returns a response which
1123 | has status code 404 (OK)
1124 | includes headers
1125 | "Content-Type" with value "application/json" (OK)
1126 | "X-Api-Correlation-Id" with value "100" (OK)
1127 | has a matching body (OK)
1128 |
1129 | A request to login with user 'sally' (250ms loading, 465ms verification)
1130 | Given User sally exists
1131 | returns a response which
1132 | has status code 200 (OK)
1133 | includes headers
1134 | "X-Api-Correlation-Id" with value "100" (OK)
1135 | "Content-Type" with value "application/json" (OK)
1136 | has a matching body (OK)
1137 |
1138 |
1139 | === RUN TestPactProvider/Provider_pact_verification
1140 | --- PASS: TestPactProvider (2.23s)
1141 | --- PASS: TestPactProvider/Provider_pact_verification (0.00s)
1142 | PASS
1143 | ok github.com/pact-foundation/pact-workshop-go/provider 2.283s
1144 | ```
1145 |
1146 | As part of this process, the results of the verification - the outcome (boolean) and the detailed information about the failures at the interaction level - are published to the Broker also.
1147 |
1148 | This is one of the Broker's more powerful features. Referred to as [Verifications](https://docs.pact.io/pact_broker/advanced_topics/provider_verification_results), it allows providers to report back the status of a verification to the broker. You'll get a quick view of the status of each consumer and provider on a nice dashboard. But, it is much more important than this!
1149 |
1150 | With just a simple use of the `pact-broker` [can-i-deploy tool](https://docs.pact.io/pact_broker/advanced_topics/provider_verification_results) - the Broker will determine if a consumer or provider is safe to release to the specified environment.
1151 |
1152 | You can run the `can-i-deploy` checks as follows:
1153 |
1154 | ```sh
1155 | $ make deploy-consumer
1156 |
1157 | --- ✅ Checking if we can deploy consumer
1158 | Computer says yes \o/
1159 |
1160 | CONSUMER | C.VERSION | PROVIDER | P.VERSION | SUCCESS?
1161 | ---------------|-----------|---------------|-----------|---------
1162 | GoAdminService | 1.0.0 | GoUserService | 1.0.0 | true
1163 |
1164 | All required verification results are published and successful
1165 |
1166 |
1167 | $ make deploy-provider
1168 |
1169 | --- ✅ Checking if we can deploy provider
1170 | Computer says yes \o/
1171 |
1172 | CONSUMER | C.VERSION | PROVIDER | P.VERSION | SUCCESS?
1173 | ---------------|-----------|---------------|-----------|---------
1174 | GoAdminService | 1.0.0 | GoUserService | 1.0.0 | true
1175 |
1176 | All required verification results are published and successful
1177 | ```
1178 |
1179 |
1180 |
1181 | That's it - you're now a Pact pro. Go build 🔨
1182 |
--------------------------------------------------------------------------------