From 738e20a0a7f95ef158d5b78fac9f9b028332e3fa Mon Sep 17 00:00:00 2001
From: Matt Fellows <matt.fellows@onegeek.com.au>
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