From 738e20a0a7f95ef158d5b78fac9f9b028332e3fa Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Sun, 20 May 2018 13:16:00 +1000 Subject: [PATCH] feat(message): tidy up Message interface - Cleaner split between `dsl` and `types` interface (`types` should not have to be directly consumable by users, and as a general rule, should only be used by the framework) - Made concepts + naming more consistent with other implementations --- README.md | 10 +- dsl/message.go | 15 +- dsl/pact.go | 141 +++++++++++------- {types => dsl}/verify_mesage_request.go | 17 ++- .../consumer/message_pact_consumer_test.go | 82 ++++++---- .../provider/message_pact_provider_test.go | 55 ++++--- examples/messages/types/types.go | 20 +++ scripts/install-cli-tools.sh | 46 +++++- types/pact_message_request.go | 1 - 9 files changed, 262 insertions(+), 125 deletions(-) rename {types => dsl}/verify_mesage_request.go (75%) create mode 100644 examples/messages/types/types.go diff --git a/README.md b/README.md index f32304241..f45a78146 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,8 @@ Read [Getting started with Pact] for more information for beginners. | Version | Stable | [Spec] Compatibility | Install | | ------- |------------|----------------------|-----------------| | 1.0.x | Yes | 2, 3* | [Installation] | -| 1.1.x | No (alpha) | 2, 3* | [release/1.1.x] | -| 0.x.x | Yes | Up to v2 | [release/0.x.x] | +| 1.1.x | No (alpha) | 2, 3* | [v1.1.x-alpha] | +| 0.x.x | Yes | Up to v2 | [v0.x.x] | _*_ v3 support is limited to the subset of functionality required to enable language inter-operable [Message support]. @@ -469,7 +469,7 @@ pact := dsl.Pact { // 4 Write the consumer test, and call VerifyMessageConsumer // passing through the function func TestMessageConsumer_Success(t *testing.T) { - message := &dsl.Message{} + message := pact.AddMessage() message. Given("some state"). ExpectsToReceive("some test case"). @@ -752,8 +752,8 @@ Detail on the native Go implementation can be found [here](https://github.com/pa See [CONTRIBUTING](CONTRIBUTING.md). [Spec]: (https://github.com/pact-foundation/pact-specification) -[release/0.x.x]: (https://github.com/pact-foundation/pact-go/tree/release/0.x.x) -[release/1.1.x]: (https://github.com/pact-foundation/pact-go/tree/release/1.1.x) +[v0.x.x]: (https://github.com/pact-foundation/pact-go/tree/release/0.x.x) +[v1.1.x-alpha]: (https://github.com/pact-foundation/pact-go/tree/release/1.1.x) [TROUBLESHOOTING]: (https://github.com/pact-foundation/pact-go/wiki/Troubleshooting) [Pact Wiki]: (https://github.com/pact-foundation/pact-ruby/wiki) [Getting started with Pact]: (http://dius.com.au/2016/02/03/microservices-pact/) diff --git a/dsl/message.go b/dsl/message.go index 6f3a26686..de4664bb6 100644 --- a/dsl/message.go +++ b/dsl/message.go @@ -5,18 +5,19 @@ import ( "reflect" ) -// type MessageHandler map[string]func(...interface{}) - // StateHandler is a provider function that sets up a given state before // the provider interaction is validated -type StateHandler func(string) (interface{}, error) +type StateHandler func(string) error + +// StateHandlers is a list of StateHandler's +type StateHandlers map[string]StateHandler -// MessageProvider is a provider function that generates a +// MessageHandler is a provider function that generates a // message for a Consumer given a Message context (state, description etc.) -type MessageProvider func(Message) (interface{}, error) +type MessageHandler func(Message) (interface{}, error) -// MessageProviders is a list of handlers ordered by description -type MessageProviders map[string]MessageProvider +// 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 diff --git a/dsl/pact.go b/dsl/pact.go index 84cf0e0ea..609fb2685 100644 --- a/dsl/pact.go +++ b/dsl/pact.go @@ -40,6 +40,9 @@ type Pact struct { // Interactions contains all of the Mock Service Interactions to be setup. Interactions []*Interaction + // MessageInteractions contains all of the Message based interactions to be setup. + MessageInteractions []*Message + // Log levels. LogLevel string @@ -89,6 +92,15 @@ type Pact struct { toolValidityCheck bool } +// AddMessage creates a new asynchronous consumer expectation +func (p *Pact) AddMessage() *Message { + log.Println("[DEBUG] pact add message") + + m := &Message{} + p.MessageInteractions = append(p.MessageInteractions, m) + return m +} + // AddInteraction creates a new Pact interaction, initialising all // required things. Will automatically start a Mock Service if none running. func (p *Pact) AddInteraction() *Interaction { @@ -316,40 +328,8 @@ var checkCliCompatibility = func() { } } -// VerifyMessageProvider accepts an instance of `*testing.T` -// running provider message verification with granular test reporting and -// automatic failure reporting for nice, simple tests. -// -// A Message Producer is analagous to Consumer in the HTTP Interaction model. -// It is the initiator of an interaction, and expects something on the other end -// of the interaction to respond - just in this case, not immediately. -func (p *Pact) VerifyMessageProvider(t *testing.T, request types.VerifyMessageRequest, handlers MessageProviders) (types.ProviderVerifierResponse, error) { - response := types.ProviderVerifierResponse{} - - // Starts the message wrapper API with hooks back to the message handlers - // This maps the 'description' field of a message pact, to a function handler - // that will implement the message producer. This function must return an object and optionally - // and error. The object will be marshalled to JSON for comparison. - mux := http.NewServeMux() - - port, err := utils.GetFreePort() - if err != nil { - return response, fmt.Errorf("unable to allocate a port for verification: %v", err) - } - - // Construct verifier request - verificationRequest := types.VerifyRequest{ - ProviderBaseURL: fmt.Sprintf("http://localhost:%d", port), - PactURLs: request.PactURLs, - BrokerURL: request.BrokerURL, - Tags: request.Tags, - BrokerUsername: request.BrokerUsername, - BrokerPassword: request.BrokerPassword, - PublishVerificationResults: request.PublishVerificationResults, - ProviderVersion: request.ProviderVersion, - } - - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +var messageHandler = func(messageHandlers MessageHandlers, stateHandlers StateHandlers) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") // Extract message @@ -364,8 +344,24 @@ func (p *Pact) VerifyMessageProvider(t *testing.T, request types.VerifyMessageRe json.Unmarshal(body, &message) + // Setup any provider state + for _, state := range message.States { + sf, stateFound := stateHandlers[state.Name] + + if !stateFound { + log.Printf("[WARN] state handler not found for state: %v", state.Name) + } else { + // Execute state handler + if err = sf(state.Name); err != nil { + log.Printf("[WARN] state handler for '%v' return error: %v", state.Name, err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + } + // Lookup key in function mapping - f, messageFound := handlers[message.Description] + f, messageFound := messageHandlers[message.Description] if !messageFound { log.Printf("[ERROR] message handler not found for message description: %v", message.Description) @@ -395,7 +391,64 @@ func (p *Pact) VerifyMessageProvider(t *testing.T, request types.VerifyMessageRe w.WriteHeader(http.StatusOK) w.Write(resBody) - }) + } +} + +// VerifyMessageProvider accepts an instance of `*testing.T` +// running provider message verification with granular test reporting and +// automatic failure reporting for nice, simple tests. +// +// A Message Producer is analagous to Consumer in the HTTP Interaction model. +// It is the initiator of an interaction, and expects something on the other end +// of the interaction to respond - just in this case, not immediately. +func (p *Pact) VerifyMessageProvider(t *testing.T, request VerifyMessageRequest) (res types.ProviderVerifierResponse, err error) { + res, err = p.VerifyMessageProviderRaw(request) + + for _, example := range res.Examples { + t.Run(example.Description, func(st *testing.T) { + st.Log(example.FullDescription) + if example.Status != "passed" { + st.Errorf("%s\n", example.Exception.Message) + st.Error("Check to ensure that all message expectations have corresponding message handlers") + } + }) + } + + return +} + +// VerifyMessageProviderRaw runs provider message verification. +// +// A Message Producer is analagous to Consumer in the HTTP Interaction model. +// It is the initiator of an interaction, and expects something on the other end +// of the interaction to respond - just in this case, not immediately. +func (p *Pact) VerifyMessageProviderRaw(request VerifyMessageRequest) (types.ProviderVerifierResponse, error) { + response := types.ProviderVerifierResponse{} + + // Starts the message wrapper API with hooks back to the message handlers + // This maps the 'description' field of a message pact, to a function handler + // that will implement the message producer. This function must return an object and optionally + // and error. The object will be marshalled to JSON for comparison. + mux := http.NewServeMux() + + port, err := utils.GetFreePort() + if err != nil { + return response, fmt.Errorf("unable to allocate a port for verification: %v", err) + } + + // Construct verifier request + verificationRequest := types.VerifyRequest{ + ProviderBaseURL: fmt.Sprintf("http://localhost:%d", port), + PactURLs: request.PactURLs, + BrokerURL: request.BrokerURL, + Tags: request.Tags, + BrokerUsername: request.BrokerUsername, + BrokerPassword: request.BrokerPassword, + PublishVerificationResults: request.PublishVerificationResults, + ProviderVersion: request.ProviderVersion, + } + + mux.HandleFunc("/", messageHandler(request.MessageHandlers, request.StateHandlers)) ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { @@ -410,23 +463,11 @@ func (p *Pact) VerifyMessageProvider(t *testing.T, request types.VerifyMessageRe sure it's running?`, port)) if portErr != nil { - t.Fatal("Error:", err) + log.Fatal("Error:", err) return response, portErr } - res, err := p.VerifyProviderRaw(verificationRequest) - - for _, example := range res.Examples { - t.Run(example.Description, func(st *testing.T) { - st.Log(example.FullDescription) - if example.Status != "passed" { - st.Errorf("%s\n", example.Exception.Message) - st.Error("Check to ensure that all message expectations have corresponding message handlers") - } - }) - } - - return res, err + return p.VerifyProviderRaw(verificationRequest) } // VerifyMessageConsumerRaw creates a new Pact _message_ interaction to build a testable diff --git a/types/verify_mesage_request.go b/dsl/verify_mesage_request.go similarity index 75% rename from types/verify_mesage_request.go rename to dsl/verify_mesage_request.go index d80d69d35..ae528677e 100644 --- a/types/verify_mesage_request.go +++ b/dsl/verify_mesage_request.go @@ -1,12 +1,11 @@ -package types +package dsl import ( "fmt" ) -// VerifyMessageRequest contains the verification params. -// TODO: make this CLI "request" type an Interface (e.g. Validate()) -// also make the core of it embeddable to be re-used +// VerifyMessageRequest contains the verification logic +// to send to the Pact Message verifier type VerifyMessageRequest struct { // Local/HTTP paths to Pact files. PactURLs []string @@ -29,6 +28,16 @@ type VerifyMessageRequest struct { // ProviderVersion is the semantical version of the Provider API. ProviderVersion string + // MessageHandlers contains a mapped list of message handlers for a provider + // that will be rable to produce the correct message format for a given + // consumer interaction + MessageHandlers MessageHandlers + + // StateHandlers contain a mapped list of message states to functions + // that are used to setup a given provider state prior to the message + // verification step. + StateHandlers StateHandlers + // Arguments to the VerificationProvider // Deprecated: This will be deleted after the native library replaces Ruby deps. Args []string diff --git a/examples/messages/consumer/message_pact_consumer_test.go b/examples/messages/consumer/message_pact_consumer_test.go index b007eb12a..652d59030 100644 --- a/examples/messages/consumer/message_pact_consumer_test.go +++ b/examples/messages/consumer/message_pact_consumer_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/pact-foundation/pact-go/dsl" + "github.com/pact-foundation/pact-go/examples/messages/types" ) var like = dsl.Like @@ -22,35 +23,11 @@ var commonHeaders = dsl.MapMatcher{ var pact = createPact() -type AccessLevel struct { - Role string `json:"role,omitempty"` -} - -type User struct { - ID int `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Access []AccessLevel `json:"access,omitempty"` -} - -var userHandlerWrapper = func(m dsl.Message) error { - return userHandler(*m.Content.(*User)) -} - -var userHandler = func(u User) error { - if u.ID == -1 { - return errors.New("invalid object supplied, missing fields (id)") - } - - // ... actually consume the message - - return nil -} - -func TestMessageConsumer_Success(t *testing.T) { - message := &dsl.Message{} +func TestMessageConsumer_UserExists(t *testing.T) { + message := pact.AddMessage() message. - Given("some state"). - ExpectsToReceive("some test case"). + Given("user with id 127 exists"). + ExpectsToReceive("a user"). WithMetadata(commonHeaders). WithContent(map[string]interface{}{ "id": like(127), @@ -59,16 +36,29 @@ func TestMessageConsumer_Success(t *testing.T) { "role": term("admin", "admin|controller|user"), }, 3), }). - AsType(&User{}) + AsType(&types.User{}) pact.VerifyMessageConsumer(t, message, userHandlerWrapper) } + +func TestMessageConsumer_Order(t *testing.T) { + message := pact.AddMessage() + message. + Given("an order exists"). + ExpectsToReceive("an order"). + WithMetadata(commonHeaders). + WithContent(dsl.Match(types.Order{})). + AsType(&types.Order{}) + + pact.VerifyMessageConsumer(t, message, orderHandlerWrapper) +} + func TestMessageConsumer_Fail(t *testing.T) { t.Skip() - message := &dsl.Message{} + message := pact.AddMessage() message. - Given("some state"). - ExpectsToReceive("some test case"). + Given("no users"). + ExpectsToReceive("a user"). WithMetadata(commonHeaders). WithContent(map[string]interface{}{ "foo": "bar", @@ -81,6 +71,34 @@ func TestMessageConsumer_Fail(t *testing.T) { }) } +var userHandlerWrapper = func(m dsl.Message) error { + return userHandler(*m.Content.(*types.User)) +} + +var orderHandlerWrapper = func(m dsl.Message) error { + return orderHandler(*m.Content.(*types.Order)) +} + +var userHandler = func(u types.User) error { + if u.ID == 0 { + return errors.New("invalid object supplied, missing fields (id)") + } + + // ... actually consume the message + + return nil +} + +var orderHandler = func(o types.Order) error { + if o.ID == 0 { + return errors.New("expected order, missing fields (id)") + } + + // ... actually consume the message + + return nil +} + // Configuration / Test Data var dir, _ = os.Getwd() var pactDir = fmt.Sprintf("%s/../../pacts", dir) diff --git a/examples/messages/provider/message_pact_provider_test.go b/examples/messages/provider/message_pact_provider_test.go index 70a51f088..eac97a4e5 100644 --- a/examples/messages/provider/message_pact_provider_test.go +++ b/examples/messages/provider/message_pact_provider_test.go @@ -7,45 +7,60 @@ import ( "testing" "github.com/pact-foundation/pact-go/dsl" - "github.com/pact-foundation/pact-go/types" + "github.com/pact-foundation/pact-go/examples/messages/types" ) -type AccessLevel struct { - Role string `json:"role,omitempty"` -} - -type User struct { - ID int `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Access []AccessLevel `json:"access,omitempty"` -} +var user *types.User // The actual Provider test itself func TestMessageProvider_Success(t *testing.T) { pact := createPact() // Map test descriptions to message producer (handlers) - // TODO: convert these all to types to ease readability - functionMappings := dsl.MessageProviders{ - "some test case": func(m dsl.Message) (interface{}, error) { - fmt.Println("Calling provider function that would produce a message") - res := User{ + functionMappings := dsl.MessageHandlers{ + "a user": func(m dsl.Message) (interface{}, error) { + if user != nil { + return user, nil + } else { + return map[string]string{ + "message": "not found", + }, nil + } + }, + "an order": func(m dsl.Message) (interface{}, error) { + return types.Order{ + ID: 1, + Item: "apple", + }, nil + }, + } + + stateMappings := dsl.StateHandlers{ + "user with id 127 exists": func(s string) error { + user = &types.User{ ID: 44, Name: "Baz", - Access: []AccessLevel{ + Access: []types.AccessLevel{ {Role: "admin"}, {Role: "admin"}, {Role: "admin"}}, } - return res, nil + return nil + }, + "no users": func(s string) error { + user = nil + + return nil }, } // Verify the Provider with local Pact Files - pact.VerifyMessageProvider(t, types.VerifyMessageRequest{ - PactURLs: []string{filepath.ToSlash(fmt.Sprintf("%s/pactgomessageconsumer-pactgomessageprovider.json", pactDir))}, - }, functionMappings) + pact.VerifyMessageProvider(t, dsl.VerifyMessageRequest{ + PactURLs: []string{filepath.ToSlash(fmt.Sprintf("%s/pactgomessageconsumer-pactgomessageprovider.json", pactDir))}, + MessageHandlers: functionMappings, + StateHandlers: stateMappings, + }) } // Configuration / Test Data diff --git a/examples/messages/types/types.go b/examples/messages/types/types.go new file mode 100644 index 000000000..7cec5518a --- /dev/null +++ b/examples/messages/types/types.go @@ -0,0 +1,20 @@ +package types + +type AccessLevel struct { + Role string `json:"role,omitempty"` +} + +type User struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Access []AccessLevel `json:"access,omitempty"` +} + +type Error struct { + Message string `json:"message" pact:"example=user not found"` +} + +type Order struct { + ID int `json:"id" pact:"example=42"` + Item string `json:"item" pact:"example=apple,regex=(apple|orange)"` +} diff --git a/scripts/install-cli-tools.sh b/scripts/install-cli-tools.sh index a96815c1c..1f730f8e3 100755 --- a/scripts/install-cli-tools.sh +++ b/scripts/install-cli-tools.sh @@ -3,21 +3,55 @@ libDir=$(dirname "$0") . "${libDir}/lib" -pactDir="build/pact" +buildDir="build" +pactDir="${buildDir}/pact" version=$(grep "var cliToolsVersion" command/version.go | grep -E -o "([0-9\.]+)") -echo "Installing CLI tools into ${libDir}" +step "Installing CLI tools locally into ${pactDir}" +log "Expecting version to be at least ${version}" +log "Installing CLI tools into ${libDir}" if [ -d "${pactDir}" ]; then + log "Removing existing directory" rm -rf ${pactDir} fi - -step "Installing CLI tools locally" mkdir -p ${pactDir} -cd build +cd ${buildDir} + +# Detect OS, default to linux 64 +uname_output=$(uname) +log "Detecting OS. Output of 'uname': ${uname_output}" +case $uname_output in + 'Linux') + linux_uname_output=$(uname -i) + case $linux_uname_output in + 'x86_64') + os='linux-x86_64' + ;; + 'i686') + os='linux-x86' + ;; + *) + log "Can't determine OS, defaulting to Linux 64bit" + os='linux-x86_64' + ;; + esac + ;; + 'Darwin') + os='osx' + ;; + *) + log "Can't determine OS, defaulting to Linux 64bit" + os='linux-x86_64' + ;; +esac + +log "OS Detected: ${os}" +log "Finding latest version from GitHub" response=$(curl -s -v https://github.com/pact-foundation/pact-ruby-standalone/releases/latest 2>&1) tag=$(echo "$response" | grep -o "Location: .*" | sed -e 's/[[:space:]]*$//' | grep -o "Location: .*" | grep -o '[^/]*$') version=${tag#v} -os="linux-x86_64" + +log "Downloading version ${version}" curl -LO https://github.com/pact-foundation/pact-ruby-standalone/releases/download/${tag}/pact-${version}-${os}.tar.gz tar xzf pact-${version}-${os}.tar.gz rm pact-${version}-${os}.tar.gz diff --git a/types/pact_message_request.go b/types/pact_message_request.go index 09ea4dc4d..c93fedf56 100644 --- a/types/pact_message_request.go +++ b/types/pact_message_request.go @@ -13,7 +13,6 @@ type PactMessageRequest struct { Consumer string // Provider is the name of the message provider - // TODO: do we always know this? Presumably not Provider string // PactDir is the location of where pacts should be stored