Skip to content

Commit

Permalink
feat: further refactoring with better type state setup
Browse files Browse the repository at this point in the history
  • Loading branch information
mefellows committed Jun 27, 2022
1 parent 672956a commit 1202555
Show file tree
Hide file tree
Showing 15 changed files with 691 additions and 298 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ download_plugins:
@echo "--- 🐿 Installing plugins"; \
./scripts/install-cli.sh
~/.pact/bin/pact-plugin-cli -y install https://github.com/pactflow/pact-protobuf-plugin/releases/tag/v-0.1.7
~/.pact/bin/pact-plugin-cli -y install https://github.com/pact-foundation/pact-plugins/releases/tag/csv-plugin-0.0.1

goveralls:
goveralls -service="travis-ci" -coverprofile=coverage.txt -repotoken $(COVERALLS_TOKEN)
Expand Down
18 changes: 16 additions & 2 deletions docs/messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The process looks like this on the provider (producer) side:

In this document, we will cover steps 1-3.

## Consumer
### Consumer

A Consumer is the system that will be reading a message from a queue or some intermediary - like a Kinesis stream, websocket or S3 bucket - and be able to handle it.

Expand Down Expand Up @@ -102,7 +102,7 @@ func TestMessagePact(t *testing.T) {
- All handlers to be tested must be of the shape `func(AsynchronousMessage) error` - that is, they must accept a `AsynchronousMessage` and return an `error`. This is how we get around all of the various protocols, and will often require a lightweight adapter function to convert it.
- In this case, we wrap the actual `userHandler` with `userHandlerWrapper` provided by Pact.

## Provider (Producer)
### Provider (Producer)

A Provider (Producer in messaging parlance) is the system that will be putting a message onto the queue.

Expand Down Expand Up @@ -159,3 +159,17 @@ func TestV3MessageProvider(t *testing.T) {
1. We configure the function mappings. In this case, we have a function that generates `a user event` which is responsible for generating the `User` event that will be sent to the consumer via some message queue
1. We setup any provider states for the interaction (see [provider](./provider.md) for more on this).
1. We configure Pact to stand-in for the queue and run the verification process. Pact will read all of the interactions specified by its consumer, invokisc each function that is responsible for generating that message and inspecting their responses


## Contract Testing (Synchronous)

In additional to "fire and forget", Pact supports bi-directional messaging protocols such as gRPC and websockets.

[Diagram TBC]

| Mode | Custom Transport | Method |
| ----- | ---------------- | ------ |
| Sync | Yes | b |
| Sync | No | c |
| Async | Yes | d |
| Async | No | e |
2 changes: 1 addition & 1 deletion examples/consumer_v3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func TestMessagePact(t *testing.T) {
}

// Message Pact - wrapped handler extracts the message
var userHandlerWrapper = func(m message.AsynchronousMessage) error {
var userHandlerWrapper = func(m message.MessageContents) error {
return userHandler(*m.Content.(*User))
}

Expand Down
21 changes: 14 additions & 7 deletions internal/native/message_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,22 @@ func (i *Message) GivenWithParameter(state string, params map[string]interface{}
cState := C.CString(state)
defer free(cState)

for k, v := range params {
cKey := C.CString(k)
defer free(cKey)
param := stringFromInterface(v)
cValue := C.CString(param)
defer free(cValue)
if len(params) == 0 {
cState := C.CString(state)
defer free(cState)

C.pactffi_message_given(i.handle, cState)
} else {
for k, v := range params {
cKey := C.CString(k)
defer free(cKey)
param := stringFromInterface(v)
cValue := C.CString(param)
defer free(cValue)

C.pactffi_message_given_with_param(i.handle, cState, cKey, cValue)
C.pactffi_message_given_with_param(i.handle, cState, cKey, cValue)

}
}

return i
Expand Down
2 changes: 1 addition & 1 deletion internal/native/message_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func TestGrpcPluginInteraction(t *testing.T) {
// m := NewPact("test-grpc-consumer", "test-plugin-provider")

// Protobuf plugin test
m.UsingPlugin("protobuf", "0.1.5")
m.UsingPlugin("protobuf", "0.1.7")
// m.WithSpecificationVersion(SPECIFICATION_VERSION_V4)

i := m.NewSyncMessageInteraction("grpc interaction")
Expand Down
4 changes: 2 additions & 2 deletions log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ func InitLogging() {
Writer: os.Stderr,
}
log.SetOutput(logFilter)
log.Println("[DEBUG] initialised logging")
}
log.Println("[DEBUG] initialised logging")

}

// SetLogLevel sets the default log level for the Pact framework
func SetLogLevel(level logutils.LogLevel) error {
InitLogging()
switch level {
case logLevelTrace, logLevelDebug, logLevelError, logLevelInfo, logLevelWarn:
logFilter.SetMinLevel(level)
Expand Down
22 changes: 0 additions & 22 deletions message/message.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package message

import (
"github.com/pact-foundation/pact-go/v2/matchers"
"github.com/pact-foundation/pact-go/v2/models"
)

Expand All @@ -15,24 +14,3 @@ type Producer Handler

// Handlers is a list of handlers ordered by description
type Handlers map[string]Handler

// AsynchronousConsumer receives a message and must be able to parse
// the content
type AsynchronousConsumer func(AsynchronousMessage) error

// type SynchronousConsumer func(SynchronousMessage) error

// V3 Message (Asynchronous only)
type AsynchronousMessage struct {
// Message Body
Content interface{} `json:"contents"`

// Provider state to be written into the Pact file
States []models.V3ProviderState `json:"providerStates"`

// Message metadata
Metadata matchers.MetadataMatcher `json:"metadata"`

// Description to be written into the Pact file
Description string `json:"description"`
}
51 changes: 28 additions & 23 deletions message/v3/asynchronous_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import (
"github.com/pact-foundation/pact-go/v2/models"
)

// Message is a representation of a single, unidirectional message
// TODO: make a builder?

// AsynchronousMessage 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 {
// AsynchronousMessage is the main implementation of the Pact AsynchronousMessage interface.
type AsynchronousMessage struct {
messageHandle *mockserver.Message
messagePactV3 *Pact
messagePactV3 *AsynchronousPact

// Type to Marshal content into when sending back to the consumer
// Defaults to interface{}
Expand All @@ -30,7 +32,7 @@ type Message struct {
}

// Given specifies a provider state. Optional.
func (m *Message) Given(state models.V3ProviderState) *Message {
func (m *AsynchronousMessage) Given(state models.V3ProviderState) *AsynchronousMessage {
m.messageHandle.GivenWithParameter(state.Name, state.Parameters)

return m
Expand All @@ -39,7 +41,7 @@ func (m *Message) Given(state models.V3ProviderState) *Message {
// ExpectsToReceive specifies the content it is expecting to be
// 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 {
func (m *AsynchronousMessage) ExpectsToReceive(description string) *AsynchronousMessage {
m.messageHandle.ExpectsToReceive(description)

return m
Expand All @@ -48,64 +50,67 @@ func (m *Message) ExpectsToReceive(description string) *Message {
// WithMetadata specifies message-implementation specific metadata
// to go with the content
// func (m *Message) WithMetadata(metadata MapMatcher) *Message {
func (m *Message) WithMetadata(metadata map[string]string) *Message {
func (m *AsynchronousMessage) WithMetadata(metadata map[string]string) *AsynchronousMessage {
m.messageHandle.WithMetadata(metadata)

return m
}

// WithBinaryContent accepts a binary payload
func (m *Message) WithBinaryContent(contentType string, body []byte) *Message {
func (m *AsynchronousMessage) WithBinaryContent(contentType string, body []byte) *AsynchronousMessage {
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 {
func (m *AsynchronousMessage) WithContent(contentType string, body []byte) *AsynchronousMessage {
m.messageHandle.WithContents(contentType, body)

return m
}

// WithJSONContent specifies the payload as an object (to be marshalled to WithJSONContent) that
// is expected to be consumed
func (m *Message) WithJSONContent(content interface{}) *Message {
func (m *AsynchronousMessage) WithJSONContent(content interface{}) *AsynchronousMessage {
m.messageHandle.WithJSONContents(content)

return m
}

// // AsType specifies that the content sent through to the
// consumer handler should be sent as the given type
func (m *Message) AsType(t interface{}) *Message {
func (m *AsynchronousMessage) AsType(t interface{}) *AsynchronousMessage {
log.Println("[DEBUG] setting Message decoding to type:", reflect.TypeOf(t))
m.Type = t

return m
}

// The function that will consume the message
func (m *Message) ConsumedBy(handler AsynchronousConsumer) *Message {
func (m *AsynchronousMessage) ConsumedBy(handler AsynchronousConsumer) *AsynchronousMessage {
m.handler = handler

return m
}

// The function that will consume the message
func (m *Message) Verify(t *testing.T) error {
func (m *AsynchronousMessage) Verify(t *testing.T) error {
return m.messagePactV3.Verify(t, m, m.handler)
}

type Pact struct {
type AsynchronousPact struct {
config Config

// Reference to the native rust handle
messageserver *mockserver.MessageServer
}

func NewMessagePact(config Config) (*Pact, error) {
provider := &Pact{
// Deprecated: use NewAsynchronousPact
var NewMessagePact = NewAsynchronousPact

func NewAsynchronousPact(config Config) (*AsynchronousPact, error) {
provider := &AsynchronousPact{
config: config,
}
err := provider.validateConfig()
Expand All @@ -120,7 +125,7 @@ func NewMessagePact(config Config) (*Pact, error) {
}

// validateConfig validates the configuration for the consumer test
func (p *Pact) validateConfig() error {
func (p *AsynchronousPact) validateConfig() error {
log.Println("[DEBUG] pact message validate config")
dir, _ := os.Getwd()

Expand All @@ -135,17 +140,17 @@ func (p *Pact) validateConfig() error {

// AddMessage creates a new asynchronous consumer expectation
// Deprecated: use AddAsynchronousMessage() instead
func (p *Pact) AddMessage() *Message {
func (p *AsynchronousPact) AddMessage() *AsynchronousMessage {
return p.AddAsynchronousMessage()
}

// AddMessage creates a new asynchronous consumer expectation
func (p *Pact) AddAsynchronousMessage() *Message {
func (p *AsynchronousPact) AddAsynchronousMessage() *AsynchronousMessage {
log.Println("[DEBUG] add message")

message := p.messageserver.NewMessage()

m := &Message{
m := &AsynchronousMessage{
messageHandle: message,
messagePactV3: p,
}
Expand All @@ -160,7 +165,7 @@ func (p *Pact) AddAsynchronousMessage() *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 *Pact) verifyMessageConsumerRaw(messageToVerify *Message, handler AsynchronousConsumer) error {
func (p *AsynchronousPact) verifyMessageConsumerRaw(messageToVerify *AsynchronousMessage, handler AsynchronousConsumer) error {
log.Printf("[DEBUG] verify message")

// 1. Strip out the matchers
Expand All @@ -169,7 +174,7 @@ func (p *Pact) verifyMessageConsumerRaw(messageToVerify *Message, handler Asynch

log.Println("[DEBUG] reified message raw", body)

var m AsynchronousMessage
var m MessageContents
err := json.Unmarshal([]byte(body), &m)
if err != nil {
return fmt.Errorf("unexpected response from message server, this is a bug in the framework")
Expand Down Expand Up @@ -206,7 +211,7 @@ func (p *Pact) verifyMessageConsumerRaw(messageToVerify *Message, handler Asynch

// VerifyMessageConsumer is a test convience function for VerifyMessageConsumerRaw,
// accepting an instance of `*testing.T`
func (p *Pact) Verify(t *testing.T, message *Message, handler AsynchronousConsumer) error {
func (p *AsynchronousPact) Verify(t *testing.T, message *AsynchronousMessage, handler AsynchronousConsumer) error {
err := p.verifyMessageConsumerRaw(message, handler)

if err != nil {
Expand Down
29 changes: 4 additions & 25 deletions message/v3/message.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,19 @@
package v3

import (
"github.com/pact-foundation/pact-go/v2/matchers"
"github.com/pact-foundation/pact-go/v2/models"
)

type Body interface{}
type Metadata map[string]interface{}

// Handler is a provider function that generates a
// message for a Consumer given a Message context (state, description etc.)
type Handler func([]models.V3ProviderState) (Body, Metadata, error)
type Producer Handler

// Handlers is a list of handlers ordered by description
type Handlers map[string]Handler

// AsynchronousConsumer receives a message and must be able to parse
// the content
type AsynchronousConsumer func(AsynchronousMessage) error

// type SynchronousConsumer func(SynchronousMessage) error
type AsynchronousConsumer func(MessageContents) error

// V3 Message (Asynchronous only)
type AsynchronousMessage struct {
type MessageContents struct {
// Message Body
Content interface{} `json:"contents"`

// Provider state to be written into the Pact file
States []models.V3ProviderState `json:"providerStates"`
Content Body `json:"contents"`

// Message metadata
Metadata matchers.MetadataMatcher `json:"metadata"`

// Description to be written into the Pact file
Description string `json:"description"`
Metadata Metadata `json:"metadata"`
}

type Config struct {
Expand Down
Loading

0 comments on commit 1202555

Please sign in to comment.