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
834 stars 104 forks source link

Empty request body during provider verification #131

Closed mikemadisonweb closed 3 years ago

mikemadisonweb commented 4 years ago

More like a question, maybe I misunderstood the concept. I'm trying to verify the provider with a local pact file I generated from the client test. All the tests are running in Docker. I have a POST request with a body, but upon verification the API handler receives an empty request, so validation fails and tests fail. I need some help here.

Software versions

Tests executing in the docker containers.

Expected behaviour

Empty request body received by API.

Actual behaviour

Request body from pact file received by API.

Steps to reproduce

This is a simplified version of the tests and the pact file:

package api

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "path/filepath"
    "testing"

    "github.com/pact-foundation/pact-go/dsl"
    "github.com/pact-foundation/pact-go/types"
    "github.com/pact-foundation/pact-go/utils"
)

func TestPactProvider(t *testing.T) {
    var port, _ = utils.GetFreePort()
    var address = fmt.Sprintf("0.0.0.0:%d", port)
    go startApi(address)

    pact := dsl.Pact{
        Provider: "api",
        Consumer: "client",
        LogDir:   os.Getenv("LOG_DIR"),
        LogLevel: "DEBUG",
    }
    _, err := pact.VerifyProvider(t, types.VerifyRequest{
        ProviderBaseURL:    fmt.Sprintf("http://%s", address),
        FailIfNoPactsFound: true,
        PactURLs:           []string{filepath.FromSlash(fmt.Sprintf("%s/client-api.json", os.Getenv("PACT_DIR")))},
        ProviderVersion:    "1.0.0",
    })

    if err != nil {
        t.Fatalf("Error on Verify: %v", err)
    }
}

func startApi(address string) {
    mux := http.NewServeMux()

    mux.HandleFunc("/v1/data", func(w http.ResponseWriter, req *http.Request) {
        w.Header().Add("Content-Type", "application/json")
        var reqRaw []byte
        req.Body.Read(reqRaw)
        fmt.Println(string(reqRaw))
        fmt.Fprintf(w, fmt.Sprintf(`%#v`, string(reqRaw)))
    })

    log.Fatal(http.ListenAndServe(address, mux))
}
{
  "consumer": {
    "name": "client"
  },
  "provider": {
    "name": "api"
  },
  "interactions": [
    {
      "description": "Client data",
      "providerState": "API response",
      "request": {
        "method": "POST",
        "path": "/v1/data",
        "body": {
          "param1": "20.23",
          "param2": "123.24"
        },
        "matchingRules": {
          "$.body.param1": {
            "match": "regex",
            "regex": "^[-+]?\\d*\\.?\\d*$"
          },
          "$.body.param2": {
            "match": "regex",
            "regex": "^[-+]?\\d*\\.?\\d*$"
          }
        }
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": {
          "result": 1.1
        },
        "matchingRules": {
          "$.headers.Content-Type": {
            "match": "regex",
            "regex": "application\\/json"
          },
          "$.body.loc": {
            "match": "type"
          }
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "2.0.0"
    }
  }
}
mefellows commented 4 years ago

I'll take a look a little later, but at first glance it looks ok. You might need to specify the content type for the request though also

mikemadisonweb commented 4 years ago

Thanks for the answer! Very strange indeed, I have created a minimal app with this pact and it worked fine: https://bitbucket.org/mikemadweb/pact-go-minimal/src/master/

I will dig deeper for an issue in the original app.

mefellows commented 4 years ago

Awesome, thanks @mikemadisonweb. It is documented in the readme, but only in the example consumer test with a comment. I'll make this for documentation enhancement because it's not immediately obvious.

Thanks for the report and taking the time to create a repro.

mikemadisonweb commented 4 years ago

Ok, so I guess I got the whole picture now. So as I mentioned previously setting Content-Type explicitly solved the case. Still why I received an empty request in the code was a mystery. I'm using Echo framework by the way and it has Bind() method, which populates the request struct depending on the request content type. If the Content-Type header was missing there would be a meaningful message that media type is not supported, seems like Pact uses application/x-www-form-urlencoded as a default. In the case of application/x-www-form-urlencoded Echo does not throw an error if the request body is JSON, but just ignores the body. That is why the request was empty.

I have updated the minimal repo with these details, just in case.

mefellows commented 4 years ago

Thanks! Yes, that all makes sense to me. Pact will do what you tell it, basically, and omitting content-type it will default to something sensible - in this case application/x-www-form-urlencoded (I believe this is standard in many frameworks as there is a body).

As mentioned, I'll make a note that if using JSON the correct header should be sent.

mefellows commented 3 years ago

Docs updated.