Skip to content

Commit 76b59b2

Browse files
feat: most basic CSV/XML support (#39)
1 parent fe8e79d commit 76b59b2

File tree

4 files changed

+163
-109
lines changed

4 files changed

+163
-109
lines changed

internal/app/pactproxy/interaction.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
const (
1818
mediaTypeJSON = "application/json"
1919
mediaTypeText = "text/plain"
20+
mediaTypeXml = "application/xml"
21+
mediaTypeCsv = "text/csv"
2022
)
2123

2224
type pathMatcher interface {
@@ -121,9 +123,9 @@ func LoadInteraction(data []byte, alias string) (*Interaction, error) {
121123
return interaction, nil
122124
}
123125
return nil, fmt.Errorf("media type is %s but body is not json", mediaType)
124-
case mediaTypeText:
125-
if plainTextRequestBody, ok := requestBody.(string); ok {
126-
interaction.addTextConstraintsFromPact(propertiesWithMatchingRule, plainTextRequestBody)
126+
case mediaTypeText, mediaTypeCsv, mediaTypeXml:
127+
if body, ok := requestBody.(string); ok {
128+
interaction.addTextConstraintsFromPact(propertiesWithMatchingRule, body)
127129
return interaction, nil
128130
}
129131
return nil, fmt.Errorf("media type is %s but body is not text", mediaType)

internal/app/pactproxy/proxy.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111
"strings"
1212
"time"
1313

14-
"github.com/form3tech-oss/pact-proxy/internal/app/httpresponse"
1514
"github.com/labstack/echo/v4"
1615
log "github.com/sirupsen/logrus"
16+
17+
"github.com/form3tech-oss/pact-proxy/internal/app/httpresponse"
1718
)
1819

1920
const (
@@ -36,6 +37,8 @@ type Config struct {
3637
var supportedMediaTypes = map[string]func([]byte, *url.URL) (requestDocument, error){
3738
mediaTypeJSON: ParseJSONRequest,
3839
mediaTypeText: ParsePlainTextRequest,
40+
mediaTypeCsv: ParsePlainTextRequest,
41+
mediaTypeXml: ParsePlainTextRequest,
3942
}
4043

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

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

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

272-
err = c.Request().Body.Close()
275+
err = req.Body.Close()
273276
if err != nil {
274277
return c.JSON(http.StatusInternalServerError, httpresponse.Error(err.Error()))
275278
}
276279

277-
c.Request().Body = io.NopCloser(bytes.NewBuffer(data))
280+
req.Body = io.NopCloser(bytes.NewBuffer(data))
278281

279-
request, err := parseRequest(data, c.Request().URL)
282+
request, err := parseRequest(data, req.URL)
280283
if err != nil {
281284
return c.JSON(http.StatusInternalServerError, httpresponse.Errorf("unable to read requestDocument data. %s", err.Error()))
282285
}
283286
h := make(map[string]interface{})
284-
for headerName, headerValues := range c.Request().Header {
287+
for headerName, headerValues := range req.Header {
285288
for _, headerValue := range headerValues {
286289
h[headerName] = headerValue
287290
}

internal/app/proxy_stage_test.go

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import (
1515
"time"
1616

1717
"github.com/avast/retry-go/v4"
18-
"github.com/form3tech-oss/pact-proxy/internal/app/configuration"
19-
internal "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy"
20-
"github.com/form3tech-oss/pact-proxy/pkg/pactproxy"
2118
"github.com/pact-foundation/pact-go/dsl"
2219
"github.com/pkg/errors"
2320
"github.com/stretchr/testify/assert"
21+
22+
"github.com/form3tech-oss/pact-proxy/internal/app/configuration"
23+
internal "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy"
24+
"github.com/form3tech-oss/pact-proxy/pkg/pactproxy"
2425
)
2526

2627
type ProxyStage struct {
@@ -224,24 +225,6 @@ func (s *ProxyStage) a_pact_that_expects_plain_text() *ProxyStage {
224225
return s
225226
}
226227

227-
func (s *ProxyStage) a_pact_that_expects_plain_text_with_request_response(req, resp string) *ProxyStage {
228-
s.pact.
229-
AddInteraction().
230-
UponReceiving(s.pactName).
231-
WithRequest(dsl.Request{
232-
Method: "POST",
233-
Path: dsl.String("/users"),
234-
Headers: dsl.MapMatcher{"Content-Type": dsl.String("text/plain")},
235-
Body: req,
236-
}).
237-
WillRespondWith(dsl.Response{
238-
Status: 200,
239-
Headers: dsl.MapMatcher{"Content-Type": dsl.String("text/plain")},
240-
Body: resp,
241-
})
242-
return s
243-
}
244-
245228
func (s *ProxyStage) a_pact_that_expects_plain_text_without_request_content_type_header() *ProxyStage {
246229
s.pact.
247230
AddInteraction().
@@ -288,14 +271,6 @@ func (s *ProxyStage) a_modified_response_attempt_of(i int) {
288271
s.modifiedAttempt = &i
289272
}
290273

291-
func (s *ProxyStage) a_plain_text_request_is_sent() {
292-
s.a_plain_text_request_is_sent_with_body("text")
293-
}
294-
295-
func (s *ProxyStage) a_plain_text_request_is_sent_with_body(body string) {
296-
s.n_requests_are_sent_using_the_body_and_content_type(1, body, "text/plain")
297-
}
298-
299274
func (s *ProxyStage) a_request_is_sent_using_the_name(name string) {
300275
s.n_requests_are_sent_using_the_name(1, name)
301276
}
@@ -451,8 +426,8 @@ func (s *ProxyStage) the_nth_response_body_has_(n int, key, value string) *Proxy
451426
return s
452427
}
453428

454-
func (s *ProxyStage) the_response_body_is(data []byte) *ProxyStage {
455-
return s.the_nth_response_body_is(1, data)
429+
func (s *ProxyStage) the_response_body_is(data string) *ProxyStage {
430+
return s.the_nth_response_body_is(1, []byte(data))
456431
}
457432

458433
func (s *ProxyStage) the_response_body_to_plain_text_request_is_correct() *ProxyStage {
@@ -539,3 +514,25 @@ func (s *ProxyStage) the_proxy_returns_details_of_all_requests() {
539514
s.assert.Equal(name, body.Name)
540515
}
541516
}
517+
518+
func (s *ProxyStage) a_pact_that_expects(reqContentType, reqBody, respContentType, respBody string) *ProxyStage {
519+
s.pact.
520+
AddInteraction().
521+
UponReceiving(s.pactName).
522+
WithRequest(dsl.Request{
523+
Method: "POST",
524+
Path: dsl.String("/users"),
525+
Headers: dsl.MapMatcher{"Content-Type": dsl.String(reqContentType)},
526+
Body: reqBody,
527+
}).
528+
WillRespondWith(dsl.Response{
529+
Status: 200,
530+
Headers: dsl.MapMatcher{"Content-Type": dsl.String(respContentType)},
531+
Body: respBody,
532+
})
533+
return s
534+
}
535+
536+
func (s *ProxyStage) a_request_is_sent_with(contentType, body string) {
537+
s.n_requests_are_sent_using_the_body_and_content_type(1, body, contentType)
538+
}

internal/app/proxy_test.go

Lines changed: 120 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -246,100 +246,152 @@ func TestModifiedBodyWithFirstAndLastName_ForNRequests(t *testing.T) {
246246
the_nth_response_body_has_(3, "last_name", "any")
247247
}
248248

249-
func TestTextPlainContentType(t *testing.T) {
250-
given, when, then := NewProxyStage(t)
251-
252-
given.
253-
a_pact_that_expects_plain_text()
254-
255-
when.
256-
a_plain_text_request_is_sent()
249+
type nonJsonTestCase struct {
250+
reqContentType string
251+
reqBody string
252+
respContentType string
253+
respBody string
254+
}
257255

258-
then.
259-
pact_verification_is_successful().and().
260-
the_response_is_(http.StatusOK).and().
261-
the_response_body_to_plain_text_request_is_correct()
256+
func createNonJsonTestCases() map[string]nonJsonTestCase {
257+
return map[string]nonJsonTestCase{
258+
// text/plain
259+
"text/plain request and text/plain response": {
260+
reqContentType: "text/plain",
261+
reqBody: "req text",
262+
respContentType: "text/plain",
263+
respBody: "resp text",
264+
},
265+
"text/plain request and application/json response": {
266+
reqContentType: "text/plain",
267+
reqBody: "req text",
268+
respContentType: "application/json",
269+
respBody: `{"status":"ok"}`,
270+
},
271+
// csv
272+
"text/csv request and text/csv response": {
273+
reqContentType: "text/csv",
274+
reqBody: "firstname,lastname\nfoo,bar",
275+
respContentType: "text/csv",
276+
respBody: "status,name\n200,ok",
277+
},
278+
"text/csv request and application/json response": {
279+
reqContentType: "text/csv",
280+
reqBody: "firstname,lastname\nfoo,bar",
281+
respContentType: "application/json",
282+
respBody: `{"status":"ok"}`,
283+
},
284+
// xml
285+
"application/xml request and text/csv response": {
286+
reqContentType: "application/xml",
287+
reqBody: "<root><firstname>foo</firstname></root>",
288+
respContentType: "application/xml",
289+
respBody: "<root><status>200</status></root>",
290+
},
291+
"application/xml request and application/json response": {
292+
reqContentType: "application/xml",
293+
reqBody: "<root><firstname>foo</firstname></root>",
294+
respContentType: "application/json",
295+
respBody: `{"status":"ok"}`,
296+
},
297+
}
262298
}
263299

264-
func TestModifiedStatusCodeWithPlainTextBody(t *testing.T) {
265-
given, when, then := NewProxyStage(t)
300+
func TestNonJsonContentType(t *testing.T) {
301+
for testName, tc := range createNonJsonTestCases() {
302+
t.Run(testName, func(t *testing.T) {
303+
given, when, then := NewProxyStage(t)
266304

267-
given.
268-
a_pact_that_expects_plain_text().and().
269-
a_modified_response_status_of_(http.StatusInternalServerError)
305+
given.
306+
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)
270307

271-
when.
272-
a_plain_text_request_is_sent()
308+
when.
309+
a_request_is_sent_with(tc.reqContentType, tc.reqBody)
310+
311+
then.
312+
pact_verification_is_successful().and().
313+
the_response_is_(http.StatusOK).and().
314+
the_response_body_is(tc.respBody)
315+
})
316+
}
273317

274-
then.
275-
pact_verification_is_successful().and().
276-
the_response_is_(http.StatusInternalServerError).and().
277-
the_response_body_to_plain_text_request_is_correct()
278318
}
279319

280-
func TestPlainTextConstraintMatches(t *testing.T) {
281-
given, when, then := NewProxyStage(t)
320+
func TestNonJsonWithModifiedStatusCode(t *testing.T) {
321+
for testName, tc := range createNonJsonTestCases() {
322+
t.Run(testName, func(t *testing.T) {
323+
given, when, then := NewProxyStage(t)
282324

283-
given.
284-
a_pact_that_expects_plain_text()
325+
given.
326+
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody).and().
327+
a_modified_response_status_of_(http.StatusInternalServerError)
285328

286-
when.
287-
a_body_constraint_is_added("text").and().
288-
a_plain_text_request_is_sent()
329+
when.
330+
a_request_is_sent_with(tc.reqContentType, tc.reqBody)
289331

290-
then.
291-
pact_verification_is_successful().and().
292-
the_response_is_(http.StatusOK).and().
293-
the_response_body_to_plain_text_request_is_correct()
332+
then.
333+
pact_verification_is_successful().and().
334+
the_response_is_(http.StatusInternalServerError).and().
335+
the_response_body_is(tc.respBody)
336+
})
337+
}
294338
}
295339

296-
func TestPlainTextDefaultConstraintAdded(t *testing.T) {
297-
given, when, then := NewProxyStage(t)
340+
func TestNonJsonConstraintMatches(t *testing.T) {
341+
for testName, tc := range createNonJsonTestCases() {
342+
t.Run(testName, func(t *testing.T) {
343+
given, when, then := NewProxyStage(t)
298344

299-
given.
300-
a_pact_that_expects_plain_text()
345+
given.
346+
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)
301347

302-
when.
303-
a_plain_text_request_is_sent_with_body("request with doesn't match constraint")
348+
when.
349+
a_body_constraint_is_added(tc.reqBody).and().
350+
a_request_is_sent_with(tc.reqContentType, tc.reqBody)
304351

305-
then.
306-
pact_verification_is_not_successful().and().
307-
the_response_is_(http.StatusBadRequest)
352+
then.
353+
pact_verification_is_successful().and().
354+
the_response_is_(http.StatusOK).and().
355+
the_response_body_is(tc.respBody)
356+
})
357+
}
308358
}
309359

310-
func TestPlainTextConstraintDoesNotMatch(t *testing.T) {
311-
given, when, then := NewProxyStage(t)
360+
func TestNonJsonDefaultConstraintAdded(t *testing.T) {
361+
for testName, tc := range createNonJsonTestCases() {
362+
t.Run(testName, func(t *testing.T) {
363+
given, when, then := NewProxyStage(t)
312364

313-
given.
314-
a_pact_that_expects_plain_text()
365+
given.
366+
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)
315367

316-
when.
317-
a_body_constraint_is_added("incorrect file content").and().
318-
a_plain_text_request_is_sent()
368+
when.
369+
a_request_is_sent_with("text/plain", "request with doesn't match constraint")
319370

320-
then.
321-
pact_verification_is_not_successful().and().
322-
the_response_is_(http.StatusBadRequest)
371+
then.
372+
pact_verification_is_not_successful().and().
373+
the_response_is_(http.StatusBadRequest)
374+
})
375+
}
323376
}
324377

325-
func TestPlainTextDifferentRequestAndResponseBodies(t *testing.T) {
326-
given, when, then := NewProxyStage(t)
327-
328-
reqBody := "request body"
329-
respBody := "response body"
330-
requestConstraint := "request body"
378+
func TestNonJsonConstraintDoesNotMatch(t *testing.T) {
379+
for testName, tc := range createNonJsonTestCases() {
380+
t.Run(testName, func(t *testing.T) {
381+
given, when, then := NewProxyStage(t)
331382

332-
given.
333-
a_pact_that_expects_plain_text_with_request_response(reqBody, respBody)
383+
given.
384+
a_pact_that_expects(tc.reqContentType, tc.reqBody, tc.respContentType, tc.respBody)
334385

335-
when.
336-
a_body_constraint_is_added(requestConstraint).and().
337-
a_plain_text_request_is_sent_with_body("request body")
386+
when.
387+
a_body_constraint_is_added("incorrect file content").and().
388+
a_request_is_sent_with(tc.reqContentType, tc.reqBody)
338389

339-
then.
340-
pact_verification_is_successful().and().
341-
the_response_is_(http.StatusOK).and().
342-
the_response_body_is([]byte(respBody))
390+
then.
391+
pact_verification_is_not_successful().and().
392+
the_response_is_(http.StatusBadRequest)
393+
})
394+
}
343395
}
344396

345397
func TestIncorrectContentTypes(t *testing.T) {

0 commit comments

Comments
 (0)