diff --git a/examples/v3/consumer_test.go b/examples/v3/consumer_test.go index 80251b4be..2f30cd3f2 100644 --- a/examples/v3/consumer_test.go +++ b/examples/v3/consumer_test.go @@ -14,6 +14,7 @@ import ( "testing" v3 "github.com/pact-foundation/pact-go/v3" + "github.com/stretchr/testify/assert" ) type s = v3.String @@ -46,41 +47,36 @@ func TestConsumerV2(t *testing.T) { AddInteraction(). Given("User foo exists"). UponReceiving("A request to do a foo"). - WithRequest(v3.Request{ - Method: "POST", - Path: v3.Regex("/foobar", `\/foo.*`), - Headers: v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": v3.Like("Bearer 1234")}, - Query: v3.QueryMatcher{ - "baz": []v3.Matcher{ - v3.Regex("bar", "[a-z]+"), - v3.Regex("bat", "[a-z]+"), - v3.Regex("baz", "[a-z]+"), - }, + WithRequest("POST", v3.Regex("/foobar", `\/foo.*`)). + Headers(v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": v3.Like("Bearer 1234")}). + Query(v3.QueryMatcher{ + "baz": []v3.Matcher{ + v3.Regex("bar", "[a-z]+"), + v3.Regex("bat", "[a-z]+"), + v3.Regex("baz", "[a-z]+"), }, - // Body: v3.MapMatcher{ - // "id": v3.Like(27), - // "name": v3.Like("billy"), - // "datetime": v3.Like("2020-01-01'T'08:00:45"), - // "lastName": v3.Like("billy"), - // }, - Body: v3.MatchV2(&User{}), }). - WillRespondWith(v3.Response{ - Status: 200, - Headers: v3.MapMatcher{"Content-Type": v3.Regex("application/json", "application\\/json")}, - // Body: v3.Match(&User{}), - Body: v3.MapMatcher{ - "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), - "name": s("FirstName"), - "lastName": s("LastName"), - "itemsMin": v3.ArrayMinLike("thereshouldbe3ofthese", 3), - // Add any of these this to demonstrate adding a v3 matcher failing the build (not at the type system level unfortunately) - // "id": v3.Integer(1), - // "superstring": v3.Includes("foo"), - // "accountBalance": v3.Decimal(123.76), - // "itemsMinMax": v3.ArrayMinMaxLike(27, 3, 5), - // "equality": v3.Equality("a thing"), - }, + // Body: v3.MatchV2(&User{}), + JSON(v3.MapMatcher{ + "id": v3.Like(27), + "name": v3.Like("billy"), + "datetime": v3.Like("2020-01-01'T'08:00:45"), + "lastName": v3.Like("billy"), + }). + WillRespondWith(200). + Headers(v3.MapMatcher{"Content-Type": v3.Regex("application/json", "application\\/json")}). + // // Body: v3.Match(&User{}), + JSON(v3.MapMatcher{ + "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), + "name": s("FirstName"), + "lastName": s("LastName"), + "itemsMin": v3.ArrayMinLike("thereshouldbe3ofthese", 3), + // Add any of these this to demonstrate adding a v3 matcher failing the build (not at the type system level unfortunately) + // "id": v3.Integer(1), + // "superstring": v3.Includes("foo"), + // "accountBalance": v3.Decimal(123.76), + // "itemsMinMax": v3.ArrayMinMaxLike(27, 3, 5), + // "equality": v3.Equality("a thing"), }) // Execute pact test @@ -107,7 +103,6 @@ func TestConsumerV3(t *testing.T) { // Set up our expected interactions. mockProvider. AddInteraction(). - // TODO: map this to given_with_param interface! Given(v3.ProviderStateV3{ Name: "User foo exists", Parameters: map[string]interface{}{ @@ -115,50 +110,41 @@ func TestConsumerV3(t *testing.T) { }, }). UponReceiving("A request to do a foo"). - WithRequest(v3.Request{ - Method: "POST", - Path: v3.Regex("/foobar", `\/foo.*`), - Headers: v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": v3.Like("Bearer 1234")}, - Body: v3.MapMatcher{ - "id": v3.Like(27), - "name": v3.FromProviderState("${name}", "billy"), - "lastName": v3.Like("billy"), - "datetime": v3.DateTimeGenerated("2020-01-01T08:00:45", "yyyy-MM-dd'T'HH:mm:ss"), - }, - - // Alternative use MatchV3 - // Body: v3.MatchV3(&User{}), - // Body: v3.MatchV2(&User{}), - Query: v3.QueryMatcher{ - "baz": []v3.Matcher{ - v3.Regex("bar", "[a-z]+"), - v3.Regex("bat", "[a-z]+"), - v3.Regex("baz", "[a-z]+"), - }, - }, + WithRequest("POST", v3.Regex("/foobar", `\/foo.*`)). + Headers(v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": v3.Like("Bearer 1234")}). + JSON(v3.MapMatcher{ + "id": v3.Like(27), + "name": v3.FromProviderState("${name}", "billy"), + "lastName": v3.Like("billy"), + "datetime": v3.DateTimeGenerated("2020-01-01T08:00:45", "yyyy-MM-dd'T'HH:mm:ss"), }). - WillRespondWith(v3.Response{ - Status: 200, - Headers: v3.MapMatcher{"Content-Type": s("application/json")}, - // Body: v3.MatchV3(&User{}), - Body: v3.MapMatcher{ - "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), - "name": s("FirstName"), - "lastName": s("LastName"), - "superstring": v3.Includes("foo"), - "id": v3.Integer(12), - "accountBalance": v3.Decimal(123.76), - "itemsMinMax": v3.ArrayMinMaxLike(27, 3, 5), - "itemsMin": v3.ArrayMinLike("thereshouldbe3ofthese", 3), - "equality": v3.Equality("a thing"), - "arrayContaining": v3.ArrayContaining([]interface{}{ - v3.Like("string"), - v3.Integer(1), - v3.MapMatcher{ - "foo": v3.Like("bar"), - }, - }), + Query(v3.QueryMatcher{ + "baz": []v3.Matcher{ + v3.Regex("bar", "[a-z]+"), + v3.Regex("bat", "[a-z]+"), + v3.Regex("baz", "[a-z]+"), }, + }). + WillRespondWith(200). + Headers(v3.MapMatcher{"Content-Type": s("application/json")}). + // Body: v3.MatchV3(&User{}), + JSON(v3.MapMatcher{ + "dateTime": v3.Regex("2020-01-01", "[0-9\\-]+"), + "name": s("FirstName"), + "lastName": s("LastName"), + "superstring": v3.Includes("foo"), + "id": v3.Integer(12), + "accountBalance": v3.Decimal(123.76), + "itemsMinMax": v3.ArrayMinMaxLike(27, 3, 5), + "itemsMin": v3.ArrayMinLike("thereshouldbe3ofthese", 3), + "equality": v3.Equality("a thing"), + "arrayContaining": v3.ArrayContaining([]interface{}{ + v3.Like("string"), + v3.Integer(1), + v3.MapMatcher{ + "foo": v3.Like("bar"), + }, + }), }) // Execute pact test @@ -168,18 +154,16 @@ func TestConsumerV3(t *testing.T) { } func TestMessagePact(t *testing.T) { + v3.SetLogLevel("TRACE") + provider, err := v3.NewMessagePactV3(v3.MessageConfig{ - Consumer: "V3MessageConsumer", - Provider: "V3MessageProvider", // must be different to the HTTP one, can't mix both interaction styles - SpecificationVersion: v3.V3, + Consumer: "V3MessageConsumer", + Provider: "V3MessageProvider", // must be different to the HTTP one, can't mix both interaction styles + // SpecificationVersion: v3.V3, }) + assert.NoError(t, err) - if err != nil { - t.Fatal(err) - } - - message := provider.AddMessage() - message. + err = provider.AddMessage(). Given(v3.ProviderStateV3{ Name: "User with id 127 exists", Parameters: map[string]interface{}{ @@ -187,18 +171,20 @@ func TestMessagePact(t *testing.T) { }, }). ExpectsToReceive("a user event"). - WithMetadata(v3.MapMatcher{ - "Content-Type": s("application/json; charset=utf-8"), + WithMetadata(map[string]string{ + "Content-Type": "application/json", }). - WithContent(v3.MapMatcher{ + JSON(v3.MapMatcher{ "datetime": v3.Regex("2020-01-01", "[0-9\\-]+"), "name": s("FirstName"), "lastName": s("LastName"), "id": v3.Integer(12), }). - AsType(&User{}) + AsType(&User{}). + ConsumedBy(userHandlerWrapper). + Verify(t) - provider.VerifyMessageConsumer(t, message, userHandlerWrapper) + assert.NoError(t, err) } type User struct { @@ -244,7 +230,7 @@ var test = func(config v3.MockServerConfig) error { } // Message Pact - wrapped handler extracts the message -var userHandlerWrapper = func(m v3.Message) error { +var userHandlerWrapper = func(m v3.AsynchronousMessage) error { return userHandler(*m.Content.(*User)) } diff --git a/v3/interaction.go b/v3/interaction.go index d0aa2d242..80fb8c695 100644 --- a/v3/interaction.go +++ b/v3/interaction.go @@ -8,15 +8,16 @@ import ( // Interaction is the main implementation of the Pact interface. type Interaction struct { - // Request - Request Request `json:"request"` - - // Response - Response Response `json:"response"` + // Reference to the native rust handle + interaction *mockserver.Interaction +} - // Description to be written into the Pact file - Description string `json:"description"` +type InteractionRequest struct { + // Reference to the native rust handle + interaction *mockserver.Interaction +} +type InteractionResponse struct { // Reference to the native rust handle interaction *mockserver.Interaction } @@ -24,7 +25,6 @@ type Interaction struct { // UponReceiving specifies the name of the test case. This becomes the name of // the consumer/provider pair in the Pact file. Mandatory. func (i *Interaction) UponReceiving(description string) *Interaction { - i.Description = description i.interaction.UponReceiving(description) return i @@ -33,33 +33,76 @@ func (i *Interaction) UponReceiving(description string) *Interaction { // WithRequest specifies the details of the HTTP request that will be used to // confirm that the Provider provides an API listening on the given interface. // Mandatory. -func (i *Interaction) WithRequest(request Request) *Interaction { - i.Request = request +func (i *Interaction) WithRequest(method Method, path Matcher) *InteractionRequest { + i.interaction.WithRequest(string(method), path) + + return &InteractionRequest{ + interaction: i.interaction, + } +} + +func (i *InteractionRequest) Method(method Method) *InteractionRequest { + + return i +} - i.interaction.WithRequest(request.Method, request.Path) - query := make(map[string][]interface{}) - for k, values := range request.Query { +func (i *InteractionRequest) Query(query QueryMatcher) *InteractionRequest { + q := make(map[string][]interface{}) + for k, values := range query { for _, v := range values { - query[k] = append(query[k], v) + q[k] = append(q[k], v) } } - i.interaction.WithQuery(query) - headers := make(map[string]interface{}) - for k, v := range request.Headers { - headers[k] = v + i.interaction.WithQuery(q) + + return i +} + +func (i *InteractionRequest) Headers(headers HeadersMatcher) *InteractionRequest { + h := make(map[string]interface{}) + for k, v := range headers { + h[k] = v } - i.interaction.WithRequestHeaders(headers) - i.interaction.WithJSONRequestBody(request.Body) + i.interaction.WithRequestHeaders(h) + + return i +} + +func (i *InteractionRequest) JSON(body interface{}) *InteractionRequest { + if s, ok := body.(string); ok { + // Check if someone tried to add an object as a string representation + // as per original allowed implementation, e.g. + // { "foo": "bar", "baz": like("bat") } + if isJSONFormattedObject(string(s)) { + log.Println("[WARN] request body appears to be a JSON formatted object, " + + "no matching will occur. Support for structured strings has been" + + "deprecated as of 0.13.0. Please use the JSON() method instead") + } + } + + i.interaction.WithJSONRequestBody(body) + + return i +} +func (i *InteractionRequest) Binary(body []byte) *InteractionRequest { + i.interaction.WithBinaryRequestBody(body) + + return i +} + +func (i *InteractionRequest) Body(contentType string, body []byte) *InteractionRequest { // Check if someone tried to add an object as a string representation // as per original allowed implementation, e.g. // { "foo": "bar", "baz": like("bat") } - if isJSONFormattedObject(request.Body) { + if isJSONFormattedObject(string(body)) { log.Println("[WARN] request body appears to be a JSON formatted object, " + - "no structural matching will occur. Support for structured strings has been" + - "deprecated as of 0.13.0") + "no matching will occur. Support for structured strings has been" + + "deprecated as of 0.13.0. Please use the JSON() method instead") } + i.interaction.WithRequestBody(contentType, body) + return i } @@ -67,28 +110,69 @@ func (i *Interaction) WithRequest(request Request) *Interaction { // confirm that the Provider must satisfy. Mandatory. // Defaults to application/json. // Use WillResponseWithContent to define custom type -func (i *Interaction) WillRespondWith(response Response) *Interaction { - return i.WillRespondWithContent("application/json", response) -} +func (i *InteractionRequest) WillRespondWith(status int) *InteractionResponse { + i.interaction.WithStatus(status) -func (i *Interaction) WillRespondWithContent(contentType string, response Response) *Interaction { - i.Response = response - headers := make(map[string]interface{}) - for k, v := range response.Headers { - headers[k] = v.(stringLike).string() + return &InteractionResponse{ + interaction: i.interaction, } - i.interaction.WithResponseHeaders(headers) - i.interaction.WithStatus(response.Status) - - if contentType == "application/json" { - i.interaction.WithJSONResponseBody(response.Body) - } else { - bodyStr, ok := response.Body.(string) - if !ok { - panic("response body must be a string") - } - i.interaction.WithResponseBody(bodyStr, contentType) +} + +func (i *InteractionResponse) Headers(headers MapMatcher) *InteractionResponse { + h := make(map[string]interface{}) + for k, v := range headers { + h[k] = v.(stringLike).string() } + i.interaction.WithResponseHeaders(h) + + return i +} + +// func (i *InteractionResponse) Status(status int) *InteractionResponse { +// i.interaction.WithStatus(status) + +// return i +// } + +func (i *InteractionResponse) JSON(body interface{}) *InteractionResponse { + i.interaction.WithJSONResponseBody(body) return i } + +func (i *InteractionResponse) Binary(body []byte) *InteractionResponse { + i.interaction.WithBinaryResponseBody(body) + + return i +} + +func (i *InteractionResponse) Body(contentType string, body []byte) *InteractionResponse { + i.interaction.WithResponseBody(contentType, body) + + return i +} + +// TODO: allow these old interfaces? +// +// func (i *InteractionResponse) WillRespondWithContent(contentType string, response Response) *InteractionResponse { +// return i.WillRespondWithContent("application/json", response) +// i.Response = response +// headers := make(map[string]interface{}) +// for k, v := range response.Headers { +// headers[k] = v.(stringLike).string() +// } +// i.interaction.WithResponseHeaders(headers) +// i.interaction.WithStatus(response.Status) + +// if contentType == "application/json" { +// i.interaction.WithJSONResponseBody(response.Body) +// } else { +// bodyStr, ok := response.Body.(string) +// if !ok { +// panic("response body must be a string") +// } +// i.interaction.WithResponseBody(contentType) +// } + +// return i +// } diff --git a/v3/interaction_v2.go b/v3/interaction_v2.go index b945c45c4..25e93e5af 100644 --- a/v3/interaction_v2.go +++ b/v3/interaction_v2.go @@ -4,15 +4,10 @@ package v3 // and is replayed on the provider side for verification type InteractionV2 struct { Interaction - - // Provider state to be written into the Pact file - State string `json:"providerState,omitempty"` } // Given specifies a provider state. Optional. func (i *InteractionV2) Given(state string) *InteractionV2 { - i.State = state - i.interaction.Given(state) return i diff --git a/v3/interaction_v3.go b/v3/interaction_v3.go index 7e082797f..989531238 100644 --- a/v3/interaction_v3.go +++ b/v3/interaction_v3.go @@ -14,14 +14,10 @@ type ProviderStateV3Response map[string]interface{} // and is replayed on the provider side for verification type InteractionV3 struct { Interaction - - // Provider state to be written into the Pact file - States []ProviderStateV3 `json:"providerStates,omitempty"` } // Given specifies a provider state. Optional. func (i *InteractionV3) Given(state ProviderStateV3) *InteractionV3 { - i.States = append(i.States, state) i.Interaction.interaction.GivenWithParameter(state.Name, state.Parameters) return i diff --git a/v3/internal/native/mockserver/message_server.go b/v3/internal/native/mockserver/message_server.go index c414658d0..6a9190ec3 100644 --- a/v3/internal/native/mockserver/message_server.go +++ b/v3/internal/native/mockserver/message_server.go @@ -148,8 +148,12 @@ func (i *Message) WithBinaryContents(body []byte) *Message { return i.WithContents("application/octet-stream", body) } -func (i *Message) WithJSONContents(body []byte) *Message { - return i.WithContents("application/json", body) +func (i *Message) WithJSONContents(body interface{}) *Message { + value := stringFromInterface(body) + + log.Println("[DEBUG] message WithJSONContents", value) + + return i.WithContents("application/json", []byte(value)) } func (i *Message) WithContents(contentType string, body []byte) *Message { diff --git a/v3/internal/native/mockserver/message_server_test.go b/v3/internal/native/mockserver/message_server_test.go index 66853650a..fbb3b501b 100644 --- a/v3/internal/native/mockserver/message_server_test.go +++ b/v3/internal/native/mockserver/message_server_test.go @@ -58,7 +58,9 @@ func TestHandleBasedMessageTestsWithJSON(t *testing.T) { WithMetadata(map[string]string{ "meta": "data", }). - WithJSONContents([]byte(`{"some": "json"}`)) + WithJSONContents(map[string]string{ + "some": "json", + }) body := m.ReifyMessage() log.Println(body) // TODO: JSON is not stringified - probably should be? diff --git a/v3/internal/native/mockserver/mock_server.go b/v3/internal/native/mockserver/mock_server.go index 04ab18d6f..e960ea584 100644 --- a/v3/internal/native/mockserver/mock_server.go +++ b/v3/internal/native/mockserver/mock_server.go @@ -16,7 +16,7 @@ typedef struct InteractionHandle InteractionHandle; struct InteractionHandle { uintptr_t pact; uintptr_t interaction; - }; +}; typedef struct MessageHandle MessageHandle; struct MessageHandle { @@ -128,7 +128,7 @@ void with_body(InteractionHandle interaction, int interaction_part, const char * void with_binary_file(InteractionHandle interaction, int interaction_part, const char *content_type, const char *body, int size); /// TODO: how to represent this? -// StringResult with_multipart_file(InteractionHandle interaction, int interaction_part, const char *content_type, const char *body, const char *part_name); +int with_multipart_file(InteractionHandle interaction, int interaction_part, const char *content_type, const char *body, const char *part_name); // https://docs.rs/pact_mock_server_ffi/0.0.7/pact_mock_server_ffi/fn.response_status.html void response_status(InteractionHandle interaction, int status); @@ -171,6 +171,11 @@ const ( INTERACTION_PART_RESPONSE ) +const ( + RESULT_OK interactionType = iota + RESULT_FAILED +) + type specificationVersion int const ( @@ -589,9 +594,6 @@ func (i *Interaction) withBinaryBody(contentType string, body []byte, part inter cHeader := C.CString(contentType) defer free(cHeader) - cBytes := C.CString(string(body)) - defer free(cBytes) - C.with_binary_file(i.handle, C.int(part), cHeader, (*C.char)(unsafe.Pointer(&body[0])), C.int(len(body))) return i @@ -605,6 +607,29 @@ func (i *Interaction) WithBinaryResponseBody(body []byte) *Interaction { return i.withBinaryBody("application/octet-stream", body, INTERACTION_PART_RESPONSE) } +func (i *Interaction) WithRequestMultipartFile(contentType string, filename string, mimePartName string) *Interaction { + return i.withMultipartFile(contentType, filename, mimePartName, INTERACTION_PART_REQUEST) +} + +func (i *Interaction) WithResponseMultipartFile(contentType string, filename string, mimePartName string) *Interaction { + return i.withMultipartFile(contentType, filename, mimePartName, INTERACTION_PART_RESPONSE) +} + +func (i *Interaction) withMultipartFile(contentType string, filename string, mimePartName string, part interactionType) *Interaction { + cHeader := C.CString(contentType) + defer free(cHeader) + + cPartName := C.CString(mimePartName) + defer free(cPartName) + + cFilename := C.CString(filename) + defer free(cFilename) + + C.with_multipart_file(i.handle, C.int(part), cHeader, cFilename, cPartName) + + return i +} + // Set the expected HTTTP response status func (i *Interaction) WithStatus(status int) *Interaction { C.response_status(i.handle, C.int(status)) diff --git a/v3/message.go b/v3/message.go index e53cb4eda..8a376736d 100644 --- a/v3/message.go +++ b/v3/message.go @@ -3,6 +3,9 @@ package v3 import ( "log" "reflect" + "testing" + + "github.com/pact-foundation/pact-go/v3/internal/native/mockserver" ) // StateHandler is a provider function that sets up a given state before @@ -19,38 +22,48 @@ type StateHandlers map[string]StateHandler // MessageHandler is a provider function that generates a // message for a Consumer given a Message context (state, description etc.) type MessageHandler func([]ProviderStateV3) (interface{}, error) +type MessageProducer MessageHandler // MessageHandlers is a list of handlers ordered by description type MessageHandlers map[string]MessageHandler // MessageConsumer receives a message and must be able to parse // the content -type MessageConsumer func(Message) error +type MessageConsumer func(AsynchronousMessage) error // Message is a representation of a single, unidirectional message // e.g. MQ, pub/sub, Websocket, Lambda // Message is the main implementation of the Pact Message interface. type Message struct { + messageHandle *mockserver.Message + messagePactV3 *MessagePactV3 + + // Type to Marshall content into when sending back to the consumer + // Defaults to interface{} + Type interface{} + + // The handler for this message + handler MessageConsumer +} + +// V3 Message (Asynchronous only) +type AsynchronousMessage struct { // Message Body - Content interface{} + Content interface{} `json:"contents"` // Provider state to be written into the Pact file - States []ProviderStateV3 + States []ProviderStateV3 `json:"providerStates"` // Message metadata - Metadata MetadataMatcher + Metadata MetadataMatcher `json:"metadata"` // Description to be written into the Pact file - Description string - - // Type to Marshall content into when sending back to the consumer - // Defaults to interface{} - Type interface{} + Description string `json:"description"` } // Given specifies a provider state. Optional. func (m *Message) Given(state ProviderStateV3) *Message { - m.States = append(m.States, state) + m.messageHandle.GivenWithParameter(state.Name, state.Parameters) return m } @@ -59,23 +72,38 @@ func (m *Message) Given(state ProviderStateV3) *Message { // given from the Provider. The function must be able to handle this // message for the interaction to succeed. func (m *Message) ExpectsToReceive(description string) *Message { - m.Description = description + m.messageHandle.ExpectsToReceive(description) + return m } // WithMetadata specifies message-implementation specific metadata // to go with the content -func (m *Message) WithMetadata(metadata MapMatcher) *Message { - m.Metadata = metadata +// func (m *Message) WithMetadata(metadata MapMatcher) *Message { +func (m *Message) WithMetadata(metadata map[string]string) *Message { + m.messageHandle.WithMetadata(metadata) + return m } -// WithContent specifies the details of the HTTP request that will be used to -// confirm that the Provider provides an API listening on the given interface. -// Mandatory. -// Content may be any of the usual structures, including binary types -func (m *Message) WithContent(content interface{}) *Message { - m.Content = content +// WithBinaryContent accepts a binary payload +func (m *Message) WithBinaryContent(contentType string, body []byte) *Message { + m.messageHandle.WithContents(contentType, body) + + return m +} + +// WithContent specifies the payload in bytes that the consumer expects to receive +func (m *Message) WithContent(contentType string, body []byte) *Message { + m.messageHandle.WithContents(contentType, body) + + return m +} + +// JSON specifies the payload as an object (to be marshalled to JSON) that +// is expected to be consumed +func (m *Message) JSON(content interface{}) *Message { + m.messageHandle.WithJSONContents(content) return m } @@ -88,3 +116,15 @@ func (m *Message) AsType(t interface{}) *Message { return m } + +// The function that will consume the message +func (m *Message) ConsumedBy(handler MessageConsumer) *Message { + m.handler = handler + + return m +} + +// The function that will consume the message +func (m *Message) Verify(t *testing.T) error { + return m.messagePactV3.Verify(t, m, m.handler) +} diff --git a/v3/message_v3.go b/v3/message_v3.go index dfed7210c..3c1848aa9 100644 --- a/v3/message_v3.go +++ b/v3/message_v3.go @@ -13,18 +13,16 @@ import ( ) type MessageConfig struct { - Consumer string - Provider string - PactDir string - SpecificationVersion SpecificationVersion + Consumer string + Provider string + PactDir string } type MessagePactV3 struct { - messages []*Message - config MessageConfig + config MessageConfig // Reference to the native rust handle - interaction *mockserver.Interaction + messageserver *mockserver.MessageServer } func NewMessagePactV3(config MessageConfig) (*MessagePactV3, error) { @@ -46,18 +44,25 @@ func (p *MessagePactV3) validateConfig() error { dir, _ := os.Getwd() if p.config.PactDir == "" { - p.config.PactDir = fmt.Sprintf(filepath.Join(dir, "pacts")) + p.config.PactDir = filepath.Join(dir, "pacts") } + p.messageserver = mockserver.NewMessageServer(p.config.Consumer, p.config.Provider) + return nil } // AddMessage creates a new asynchronous consumer expectation func (p *MessagePactV3) AddMessage() *Message { - log.Println("[DEBUG] pact add message") + log.Println("[DEBUG] add message") + + message := p.messageserver.NewMessage() + + m := &Message{ + messageHandle: message, + messagePactV3: p, + } - m := &Message{} - p.messages = append(p.messages, m) return m } @@ -68,58 +73,54 @@ func (p *MessagePactV3) AddMessage() *Message { // A Message Consumer is analagous to a Provider in the HTTP Interaction model. // It is the receiver of an interaction, and needs to be able to handle whatever // request was provided. -func (p *MessagePactV3) VerifyMessageConsumerRaw(message *Message, handler MessageConsumer) error { +func (p *MessagePactV3) verifyMessageConsumerRaw(message *Message, handler MessageConsumer) error { log.Printf("[DEBUG] verify message") // 1. Strip out the matchers // Reify the message back to its "example/generated" form - body, _, _ := buildPart(message.Content) + body := message.messageHandle.ReifyMessage() + + log.Println("[DEBUG] reified message raw", body) + + var m AsynchronousMessage + err := json.Unmarshal([]byte(body), &m) + if err != nil { + return fmt.Errorf("unexpected response from message server, this is a bug in the framework") + } + log.Println("[DEBUG] unmarshalled into an AsynchronousMessage", m) // 2. Convert to an actual type (to avoid wrapping if needed/requested) // 3. Invoke the message handler // 4. write the pact file - // - fail if http interactions are already present - // - merge interactions - // - only append interactions if a general structure already present - t := reflect.TypeOf(message.Type) if t != nil && t.Name() != "interface" { - reified, err := json.Marshal(body) + s, err := json.Marshal(m.Content) if err != nil { return fmt.Errorf("unable to generate message for type: %+v", message.Type) } - err = json.Unmarshal(reified, &message.Type) + err = json.Unmarshal(s, &message.Type) if err != nil { return fmt.Errorf("unable to narrow type to %v: %v", t.Name(), err) } + + m.Content = message.Type } // Yield message, and send through handler function - generatedMessage := - Message{ - Content: message.Type, - States: message.States, - Description: message.Description, - Metadata: message.Metadata, - } + err = handler(m) - err := handler(generatedMessage) if err != nil { return err } - // // If no errors, update Message Pact - // Generate interactions for Pact file - serialisedPact := newPactFileV3(p.config.Consumer, p.config.Provider, nil, p.messages) - - return p.pactWriter.update(p.config.PactDir, serialisedPact) + return p.messageserver.WritePactFile(p.config.PactDir, false) } // VerifyMessageConsumer is a test convience function for VerifyMessageConsumerRaw, // accepting an instance of `*testing.T` -func (p *MessagePactV3) VerifyMessageConsumer(t *testing.T, message *Message, handler MessageConsumer) error { - err := p.VerifyMessageConsumerRaw(message, handler) +func (p *MessagePactV3) Verify(t *testing.T, message *Message, handler MessageConsumer) error { + err := p.verifyMessageConsumerRaw(message, handler) if err != nil { t.Errorf("VerifyMessageConsumer failed: %v", err) diff --git a/v3/verify_request.go b/v3/verify_request.go index b5df29218..809a03a99 100644 --- a/v3/verify_request.go +++ b/v3/verify_request.go @@ -150,7 +150,7 @@ func (v *VerifyRequest) validate() error { } if len(v.PactURLs) == 0 && len(v.PactFiles) == 0 && len(v.PactDirs) == 0 && v.BrokerURL == "" { - return fmt.Errorf("One of 'PactURLs', 'PactFiles', 'PactDIRs' or 'BrokerURL' must be specified") + return fmt.Errorf("one of 'PactURLs', 'PactFiles', 'PactDIRs' or 'BrokerURL' must be specified") } // TODO: @@ -262,7 +262,7 @@ func (v *VerifyRequest) validate() error { v.args = append(v.args, "--provider-tags", strings.Join(v.ProviderTags, ",")) } - v.args = append(v.args, "--loglevel", strings.ToLower(fmt.Sprintf("%s", LogLevel()))) + v.args = append(v.args, "--loglevel", strings.ToLower(string(LogLevel()))) if len(v.Tags) > 0 { v.args = append(v.args, "--consumer-version-tags", strings.Join(v.Tags, ",")) @@ -297,7 +297,7 @@ func (v *VerifyRequest) verify(writer outputWriter) error { fmt.Sprintf(`Timed out waiting for Provider API to start on port %d - are you sure it's running?`, port)) service := native.Verifier{} - service.Init() + native.Init() res := service.Verify(v.args) return res