Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provider verifier not working correctly when consumer uses matchers.Decimal #329

Open
AnhTaFP opened this issue Aug 5, 2023 · 2 comments
Labels
awaiting feedback Awaiting Feedback from OP

Comments

@AnhTaFP
Copy link

AnhTaFP commented Aug 5, 2023

Software versions

  • OS: e.g. Mac OS Monterey 12.6.8
  • Consumer Pact library: pact-go v2.0.1
  • Provider Pact library: pact-go v2.0.1
  • Golang Version: Go v1.20
  • Golang environment: Provide output of go env
GO111MODULE=""
GOARCH="arm64"
GOBIN=""
GOCACHE="/Users/some.user/Library/Caches/go-build"
GOENV="/Users/some.user/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="arm64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/some.user/go/pkg/mod"
GOOS="darwin"
GOPATH="/Users/some.user/go"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/Users/some.user/go/go1.20.6"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/some.user/go/go1.20.6/pkg/tool/darwin_arm64"
GOVCS=""
GOVERSION="go1.20.6"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/03/m_znf89s593bpkdgwytb9tzjdh18pw/T/go-build578168331=/tmp/go-build -gno-record-gcc-switches -fno-common"

Expected behaviour

5.0 should be a valid decimal value when running provider verifier

Actual behaviour

5.0 is not considered a valid decimal value when running provider verifier

Steps to reproduce

  • Create package consumer and its test.
// this is consumer.go
package consumer

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type ProductConsumer struct {
	host string
}

func NewProductConsumer(host string) *ProductConsumer {
	return &ProductConsumer{
		host: host,
	}
}

func (c *ProductConsumer) GetProduct(id int) (*Product, error) {
	url := fmt.Sprintf("%s/products/%d", c.host, id)
	resp, err := http.DefaultClient.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	b, _ := io.ReadAll(resp.Body)

	var p Product
	_ = json.Unmarshal(b, &p)

	return &p, nil
}

type Product struct {
	ID    int     `json:"id"`
	Price float64 `json:"price"`
}
// this is consumer_test.go
package consumer 

import (
	"fmt"
	"testing"

	"github.com/pact-foundation/pact-go/v2/consumer"
	"github.com/pact-foundation/pact-go/v2/matchers"
	"github.com/stretchr/testify/assert"
)

func TestProductConsumer(t *testing.T) {
	mockProvider, err := consumer.NewV4Pact(consumer.MockHTTPProviderConfig{
		Consumer: "product-consumer",
		Provider: "product-provider",
		Port:     8089,
	})

	err = mockProvider.
		AddInteraction().
		Given("product #1 exists").
		UponReceiving("a request to get product #1").
		WithRequest("GET", "/products/1").
		WillRespondWith(200, func(b *consumer.V4ResponseBuilder) {
			b.JSONBody(matchers.Map{
				"id":    matchers.Integer(1),
				"price": matchers.Decimal(5.0),
			})
		}).
		ExecuteTest(t, func(config consumer.MockServerConfig) error {
			c := NewProductConsumer(fmt.Sprintf("http://%s:%d", config.Host, config.Port))
			p, err := c.GetProduct(1)

			assert.NoError(t, err)
			assert.Equal(t, 1, p.ID)

			return nil
		})

	assert.NoError(t, err)
}
  • Run the test, get the output of the pact
{
  "consumer": {
    "name": "product-consumer"
  },
  "interactions": [
    {
      "description": "a request to get product #1",
      "pending": false,
      "providerStates": [
        {
          "name": "product #1 exists"
        }
      ],
      "request": {
        "method": "GET",
        "path": "/products/1"
      },
      "response": {
        "body": {
          "content": {
            "id": 1,
            "price": 5
          },
          "contentType": "application/json",
          "encoded": false
        },
        "headers": {
          "Content-Type": [
            "application/json"
          ]
        },
        "matchingRules": {
          "body": {
            "$.id": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "integer"
                }
              ]
            },
            "$.price": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "decimal"
                }
              ]
            }
          }
        },
        "status": 200
      },
      "transport": "http",
      "type": "Synchronous/HTTP"
    }
  ],
  "metadata": {
    "pactRust": {
      "ffi": "0.4.5",
      "mockserver": "1.1.1",
      "models": "1.1.2"
    },
    "pactSpecification": {
      "version": "4.0"
    }
  },
  "provider": {
    "name": "product-provider"
  }
}
  • Create a provider test
package productprovider

import (
	"encoding/json"
	"net/http"
	"testing"

	"github.com/pact-foundation/pact-go/v2/models"
	"github.com/pact-foundation/pact-go/v2/provider"
	"github.com/stretchr/testify/assert"
)

func startServer() {
	type product struct {
		ID    int     `json:"id"`
		Price float64 `json:"price"`
	}

	http.HandleFunc("/products/1", func(writer http.ResponseWriter, request *http.Request) {
		d := product{
			ID:    1,
			Price: 5.0,
		}
		b, _ := json.Marshal(d)

		writer.Header().Set("Content-Type", "application/json")
		writer.Write(b)
	})

	http.ListenAndServe("localhost:8080", nil)
}

func TestProductProvider(t *testing.T) {
	go startServer()

	v := provider.NewVerifier()

	err := v.VerifyProvider(t, provider.VerifyRequest{
		ProviderBaseURL: "http://localhost:8080",
		Provider:        "product-provider",
		ProviderVersion: "product-provider-v1.0",
		PactDirs:        []string{"/path/to/consumer/pacts"},
		StateHandlers: models.StateHandlers{
			"product #1 exists": func(setup bool, state models.ProviderState) (models.ProviderStateResponse, error) {
				return models.ProviderStateResponse{}, nil
			},
		},
	})

	assert.NoError(t, err)
}
  • Run the provider test, it fails with this log
=== RUN   TestProductProvider
2023-08-05T14:11:11.107350Z  INFO ThreadId(11) pact_verifier: Running setup provider state change handler 'product #1 exists' for 'a request to get product #1'
2023/08/05 21:11:11 [INFO] executing state handler middleware
2023-08-05T14:11:11.287108Z  INFO ThreadId(11) pact_verifier: Running provider verification for 'a request to get product #1'
2023-08-05T14:11:11.287166Z  INFO ThreadId(11) pact_verifier::provider_client: Sending request to provider at http://localhost:52872/
2023-08-05T14:11:11.287168Z  INFO ThreadId(11) pact_verifier::provider_client: Sending request HTTP Request ( method: GET, path: /products/1, query: None, headers: None, body: Missing )
2023-08-05T14:11:11.288802Z  INFO ThreadId(11) pact_verifier::provider_client: Received response: HTTP Response ( status: 200, headers: Some({"content-length": ["18"], "content-type": ["application/json"], "date": ["Sat, 05 Aug 2023 14:11:11 GMT"]}), body: Present(18 bytes, application/json) )
2023-08-05T14:11:11.288821Z  INFO ThreadId(11) pact_matching: comparing to expected response: HTTP Response ( status: 200, headers: Some({"Content-Type": ["application/json"]}), body: Present(18 bytes, application/json) )
2023-08-05T14:11:11.290069Z  INFO ThreadId(11) pact_verifier: Running teardown provider state change handler 'product #1 exists' for 'a request to get product #1'
2023/08/05 21:11:11 [INFO] executing state handler middleware

Verifying a pact between product-consumer and product-provider

  a request to get product #1 (0s loading, 522ms verification)
     Given product #1 exists
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (OK)
      has a matching body (FAILED)


Failures:

1) Verifying a pact between product-consumer and product-provider Given product #1 exists - a request to get product #1
    1.1) has a matching body
           $.price -> Expected '5' to be a decimal value


There were 1 pact failures

    productprovider_test.go:52: 
        	Error Trace:	/Users/some.user/path/to/productprovider/productprovider_test.go:52
        	Error:      	Received unexpected error:
        	            	the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
        	Test:       	TestProductProvider
--- FAIL: TestProductProvider (0.58s)
=== RUN   TestProductProvider/Provider_pact_verification
    verifier.go:184: the verifier failed to successfully verify the pacts, this indicates an issue with the provider API
    --- FAIL: TestProductProvider/Provider_pact_verification (0.00s)


FAIL

Process finished with the exit code 1

Relevent log files

N/A

Please note that if I change the dummy value in provider server from 5.0 to 5.1 then the provider verifier test works.

@mefellows
Copy link
Member

Thanks for the detailed report.

This is likely a limitation of the JSON parser. It seems somewhere in the chain 5.0 is being reduced to simply 5.

Can you confirm that what is sent over the wire is 5 or 5.0? In JS I know it will be 5 even if a decimal is provided. As such there's not much a test framework can do if it doesn't receive a value with decimal places.

@mefellows mefellows added the awaiting feedback Awaiting Feedback from OP label Aug 5, 2023
@AnhTaFP
Copy link
Author

AnhTaFP commented Aug 6, 2023

I think you're right, I wrote another small program to check the ouput of the json marshaller in Go in this case

package main

import (
	"encoding/json"
	"log"
	"net/http"
)

func main() {
	type product struct {
		ID    int     `json:"id"`
		Price float64 `json:"price"`
	}

	http.HandleFunc("/products/1", func(writer http.ResponseWriter, request *http.Request) {
		d := product{
			ID:    1,
			Price: 5.0,
		}
		b, _ := json.Marshal(d)
		log.Println("product is marshalled into", string(b))

		writer.Header().Set("Content-Type", "application/json")
		writer.Write(b)
	})

	http.ListenAndServe("localhost:8080", nil)
}

I called the end-point, and here's the log

2023/08/06 09:54:38 product is marshalled into {"id":1,"price":5}

It looks like the json marshaller in Go truncates 5.0 to 5, so the behavior is the same as JS. I don't know how to overcome this for now (except writing my own marshaller), maybe just make sure that the provider verifier tests avoiding decimal with only 0 after the decimal point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting feedback Awaiting Feedback from OP
Projects
Status: New Issue
Development

No branches or pull requests

2 participants