├── 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 |