pact-foundation / pact-go

Golang version of Pact. Pact is a contract testing framework for HTTP APIs and non-HTTP asynchronous messaging systems.
http://pact.io
MIT License
862 stars 110 forks source link

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

Open AnhTaFP opened 1 year ago

AnhTaFP commented 1 year ago

Software versions

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

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" }


```go
// 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)
}

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 commented 1 year ago

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.

AnhTaFP commented 1 year ago

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.