Skip to content

Commit

Permalink
feat(message): tidy up Message interface
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
mefellows committed May 20, 2018
1 parent 2e0f236 commit 738e20a
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 125 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].

Expand Down Expand Up @@ -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").
Expand Down Expand Up @@ -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/)
Expand Down
15 changes: 8 additions & 7 deletions dsl/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
141 changes: 91 additions & 50 deletions dsl/pact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
17 changes: 13 additions & 4 deletions types/verify_mesage_request.go → dsl/verify_mesage_request.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
82 changes: 50 additions & 32 deletions examples/messages/consumer/message_pact_consumer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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",
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 738e20a

Please sign in to comment.