Skip to content

Commit

Permalink
feat: most basic CSV/XML support (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
damiankaminski-form3 authored Feb 27, 2023
1 parent fe8e79d commit 76b59b2
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 109 deletions.
8 changes: 5 additions & 3 deletions internal/app/pactproxy/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
const (
mediaTypeJSON = "application/json"
mediaTypeText = "text/plain"
mediaTypeXml = "application/xml"
mediaTypeCsv = "text/csv"
)

type pathMatcher interface {
Expand Down Expand Up @@ -121,9 +123,9 @@ func LoadInteraction(data []byte, alias string) (*Interaction, error) {
return interaction, nil
}
return nil, fmt.Errorf("media type is %s but body is not json", mediaType)
case mediaTypeText:
if plainTextRequestBody, ok := requestBody.(string); ok {
interaction.addTextConstraintsFromPact(propertiesWithMatchingRule, plainTextRequestBody)
case mediaTypeText, mediaTypeCsv, mediaTypeXml:
if body, ok := requestBody.(string); ok {
interaction.addTextConstraintsFromPact(propertiesWithMatchingRule, body)
return interaction, nil
}
return nil, fmt.Errorf("media type is %s but body is not text", mediaType)
Expand Down
17 changes: 10 additions & 7 deletions internal/app/pactproxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (
"strings"
"time"

"github.com/form3tech-oss/pact-proxy/internal/app/httpresponse"
"github.com/labstack/echo/v4"
log "github.com/sirupsen/logrus"

"github.com/form3tech-oss/pact-proxy/internal/app/httpresponse"
)

const (
Expand All @@ -36,6 +37,8 @@ type Config struct {
var supportedMediaTypes = map[string]func([]byte, *url.URL) (requestDocument, error){
mediaTypeJSON: ParseJSONRequest,
mediaTypeText: ParsePlainTextRequest,
mediaTypeCsv: ParsePlainTextRequest,
mediaTypeXml: ParsePlainTextRequest,
}

type api struct {
Expand Down Expand Up @@ -247,9 +250,9 @@ func (a *api) interactionsWaitHandler(c echo.Context) error {

func (a *api) indexHandler(c echo.Context) error {
req := c.Request()
log.Infof("proxying %s %s", req.Method, req.URL.Path)
log.Infof("proxying %s %s %+v", req.Method, req.URL.Path, req.Header)

mediaType, err := parseMediaTypeHeader(c.Request().Header)
mediaType, err := parseMediaTypeHeader(req.Header)
if err != nil {
return c.JSON(http.StatusBadRequest, httpresponse.Errorf("failed to parse Content-Type header. %s", err.Error()))
}
Expand All @@ -269,19 +272,19 @@ func (a *api) indexHandler(c echo.Context) error {
return c.JSON(http.StatusBadRequest, httpresponse.Errorf("unable to read requestDocument data. %s", err.Error()))
}

err = c.Request().Body.Close()
err = req.Body.Close()
if err != nil {
return c.JSON(http.StatusInternalServerError, httpresponse.Error(err.Error()))
}

c.Request().Body = io.NopCloser(bytes.NewBuffer(data))
req.Body = io.NopCloser(bytes.NewBuffer(data))

request, err := parseRequest(data, c.Request().URL)
request, err := parseRequest(data, req.URL)
if err != nil {
return c.JSON(http.StatusInternalServerError, httpresponse.Errorf("unable to read requestDocument data. %s", err.Error()))
}
h := make(map[string]interface{})
for headerName, headerValues := range c.Request().Header {
for headerName, headerValues := range req.Header {
for _, headerValue := range headerValues {
h[headerName] = headerValue
}
Expand Down
59 changes: 28 additions & 31 deletions internal/app/proxy_stage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import (
"time"

"github.com/avast/retry-go/v4"
"github.com/form3tech-oss/pact-proxy/internal/app/configuration"
internal "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy"
"github.com/form3tech-oss/pact-proxy/pkg/pactproxy"
"github.com/pact-foundation/pact-go/dsl"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"

"github.com/form3tech-oss/pact-proxy/internal/app/configuration"
internal "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy"
"github.com/form3tech-oss/pact-proxy/pkg/pactproxy"
)

type ProxyStage struct {
Expand Down Expand Up @@ -224,24 +225,6 @@ func (s *ProxyStage) a_pact_that_expects_plain_text() *ProxyStage {
return s
}

func (s *ProxyStage) a_pact_that_expects_plain_text_with_request_response(req, resp string) *ProxyStage {
s.pact.
AddInteraction().
UponReceiving(s.pactName).
WithRequest(dsl.Request{
Method: "POST",
Path: dsl.String("/users"),
Headers: dsl.MapMatcher{"Content-Type": dsl.String("text/plain")},
Body: req,
}).
WillRespondWith(dsl.Response{
Status: 200,
Headers: dsl.MapMatcher{"Content-Type": dsl.String("text/plain")},
Body: resp,
})
return s
}

func (s *ProxyStage) a_pact_that_expects_plain_text_without_request_content_type_header() *ProxyStage {
s.pact.
AddInteraction().
Expand Down Expand Up @@ -288,14 +271,6 @@ func (s *ProxyStage) a_modified_response_attempt_of(i int) {
s.modifiedAttempt = &i
}

func (s *ProxyStage) a_plain_text_request_is_sent() {
s.a_plain_text_request_is_sent_with_body("text")
}

func (s *ProxyStage) a_plain_text_request_is_sent_with_body(body string) {
s.n_requests_are_sent_using_the_body_and_content_type(1, body, "text/plain")
}

func (s *ProxyStage) a_request_is_sent_using_the_name(name string) {
s.n_requests_are_sent_using_the_name(1, name)
}
Expand Down Expand Up @@ -451,8 +426,8 @@ func (s *ProxyStage) the_nth_response_body_has_(n int, key, value string) *Proxy
return s
}

func (s *ProxyStage) the_response_body_is(data []byte) *ProxyStage {
return s.the_nth_response_body_is(1, data)
func (s *ProxyStage) the_response_body_is(data string) *ProxyStage {
return s.the_nth_response_body_is(1, []byte(data))
}

func (s *ProxyStage) the_response_body_to_plain_text_request_is_correct() *ProxyStage {
Expand Down Expand Up @@ -539,3 +514,25 @@ func (s *ProxyStage) the_proxy_returns_details_of_all_requests() {
s.assert.Equal(name, body.Name)
}
}

func (s *ProxyStage) a_pact_that_expects(reqContentType, reqBody, respContentType, respBody string) *ProxyStage {
s.pact.
AddInteraction().
UponReceiving(s.pactName).
WithRequest(dsl.Request{
Method: "POST",
Path: dsl.String("/users"),
Headers: dsl.MapMatcher{"Content-Type": dsl.String(reqContentType)},
Body: reqBody,
}).
WillRespondWith(dsl.Response{
Status: 200,
Headers: dsl.MapMatcher{"Content-Type": dsl.String(respContentType)},
Body: respBody,
})
return s
}

func (s *ProxyStage) a_request_is_sent_with(contentType, body string) {
s.n_requests_are_sent_using_the_body_and_content_type(1, body, contentType)
}
188 changes: 120 additions & 68 deletions internal/app/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,100 +246,152 @@ func TestModifiedBodyWithFirstAndLastName_ForNRequests(t *testing.T) {
the_nth_response_body_has_(3, "last_name", "any")
}

func TestTextPlainContentType(t *testing.T) {
given, when, then := NewProxyStage(t)

given.
a_pact_that_expects_plain_text()

when.
a_plain_text_request_is_sent()
type nonJsonTestCase struct {
reqContentType string
reqBody string
respContentType string
respBody string
}

then.
pact_verification_is_successful().and().
the_response_is_(http.StatusOK).and().
the_response_body_to_plain_text_request_is_correct()
func createNonJsonTestCases() map[string]nonJsonTestCase {
return map[string]nonJsonTestCase{
// text/plain
"text/plain request and text/plain response": {
reqContentType: "text/plain",
reqBody: "req text",
respContentType: "text/plain",
respBody: "resp text",
},
"text/plain request and application/json response": {
reqContentType: "text/plain",
reqBody: "req text",
respContentType: "application/json",
respBody: `{"status":"ok"}`,
},
// csv
"text/csv request and text/csv response": {
reqContentType: "text/csv",
reqBody: "firstname,lastname\nfoo,bar",
respContentType: "text/csv",
respBody: "status,name\n200,ok",
},
"text/csv request and application/json response": {
reqContentType: "text/csv",
reqBody: "firstname,lastname\nfoo,bar",
respContentType: "application/json",
respBody: `{"status":"ok"}`,
},
// xml
"application/xml request and text/csv response": {
reqContentType: "application/xml",
reqBody: "<root><firstname>foo</firstname></root>",
respContentType: "application/xml",
respBody: "<root><status>200</status></root>",
},
"application/xml request and application/json response": {
reqContentType: "application/xml",
reqBody: "<root><firstname>foo</firstname></root>",
respContentType: "application/json",
respBody: `{"status":"ok"}`,
},
}
}

func TestModifiedStatusCodeWithPlainTextBody(t *testing.T) {
given, when, then := NewProxyStage(t)
func TestNonJsonContentType(t *testing.T) {
for testName, tc := range createNonJsonTestCases() {
t.Run(testName, func(t *testing.T) {
given, when, then := NewProxyStage(t)

given.
a_pact_that_expects_plain_text().and().
a_modified_response_status_of_(http.StatusInternalServerError)
given.
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)

when.
a_plain_text_request_is_sent()
when.
a_request_is_sent_with(tc.reqContentType, tc.reqBody)

then.
pact_verification_is_successful().and().
the_response_is_(http.StatusOK).and().
the_response_body_is(tc.respBody)
})
}

then.
pact_verification_is_successful().and().
the_response_is_(http.StatusInternalServerError).and().
the_response_body_to_plain_text_request_is_correct()
}

func TestPlainTextConstraintMatches(t *testing.T) {
given, when, then := NewProxyStage(t)
func TestNonJsonWithModifiedStatusCode(t *testing.T) {
for testName, tc := range createNonJsonTestCases() {
t.Run(testName, func(t *testing.T) {
given, when, then := NewProxyStage(t)

given.
a_pact_that_expects_plain_text()
given.
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody).and().
a_modified_response_status_of_(http.StatusInternalServerError)

when.
a_body_constraint_is_added("text").and().
a_plain_text_request_is_sent()
when.
a_request_is_sent_with(tc.reqContentType, tc.reqBody)

then.
pact_verification_is_successful().and().
the_response_is_(http.StatusOK).and().
the_response_body_to_plain_text_request_is_correct()
then.
pact_verification_is_successful().and().
the_response_is_(http.StatusInternalServerError).and().
the_response_body_is(tc.respBody)
})
}
}

func TestPlainTextDefaultConstraintAdded(t *testing.T) {
given, when, then := NewProxyStage(t)
func TestNonJsonConstraintMatches(t *testing.T) {
for testName, tc := range createNonJsonTestCases() {
t.Run(testName, func(t *testing.T) {
given, when, then := NewProxyStage(t)

given.
a_pact_that_expects_plain_text()
given.
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)

when.
a_plain_text_request_is_sent_with_body("request with doesn't match constraint")
when.
a_body_constraint_is_added(tc.reqBody).and().
a_request_is_sent_with(tc.reqContentType, tc.reqBody)

then.
pact_verification_is_not_successful().and().
the_response_is_(http.StatusBadRequest)
then.
pact_verification_is_successful().and().
the_response_is_(http.StatusOK).and().
the_response_body_is(tc.respBody)
})
}
}

func TestPlainTextConstraintDoesNotMatch(t *testing.T) {
given, when, then := NewProxyStage(t)
func TestNonJsonDefaultConstraintAdded(t *testing.T) {
for testName, tc := range createNonJsonTestCases() {
t.Run(testName, func(t *testing.T) {
given, when, then := NewProxyStage(t)

given.
a_pact_that_expects_plain_text()
given.
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)

when.
a_body_constraint_is_added("incorrect file content").and().
a_plain_text_request_is_sent()
when.
a_request_is_sent_with("text/plain", "request with doesn't match constraint")

then.
pact_verification_is_not_successful().and().
the_response_is_(http.StatusBadRequest)
then.
pact_verification_is_not_successful().and().
the_response_is_(http.StatusBadRequest)
})
}
}

func TestPlainTextDifferentRequestAndResponseBodies(t *testing.T) {
given, when, then := NewProxyStage(t)

reqBody := "request body"
respBody := "response body"
requestConstraint := "request body"
func TestNonJsonConstraintDoesNotMatch(t *testing.T) {
for testName, tc := range createNonJsonTestCases() {
t.Run(testName, func(t *testing.T) {
given, when, then := NewProxyStage(t)

given.
a_pact_that_expects_plain_text_with_request_response(reqBody, respBody)
given.
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)

when.
a_body_constraint_is_added(requestConstraint).and().
a_plain_text_request_is_sent_with_body("request body")
when.
a_body_constraint_is_added("incorrect file content").and().
a_request_is_sent_with(tc.reqContentType, tc.reqBody)

then.
pact_verification_is_successful().and().
the_response_is_(http.StatusOK).and().
the_response_body_is([]byte(respBody))
then.
pact_verification_is_not_successful().and().
the_response_is_(http.StatusBadRequest)
})
}
}

func TestIncorrectContentTypes(t *testing.T) {
Expand Down

0 comments on commit 76b59b2

Please sign in to comment.