From b017cad30d3d78a9df42148165655cfc0be47d5e Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Tue, 7 Jun 2022 13:57:12 +1000 Subject: [PATCH] refactor: improve segregation between HTTP and Message based functions --- consumer/http.go | 2 +- docs/troubleshooting.md | 2 +- internal/native/message_server.go | 261 +++++++++++++++++++++++-- internal/native/message_server_test.go | 118 ++++++++++- internal/native/mock_server.go | 30 +-- internal/native/mock_server_test.go | 88 +-------- 6 files changed, 371 insertions(+), 130 deletions(-) diff --git a/consumer/http.go b/consumer/http.go index 8423814be..fa9f2f818 100644 --- a/consumer/http.go +++ b/consumer/http.go @@ -115,7 +115,7 @@ func (p *httpMockProvider) configure() error { p.config.ClientTimeout = 10 * time.Second } - p.mockserver = native.NewHTTPMockServer(p.config.Consumer, p.config.Provider) + p.mockserver = native.NewPact(p.config.Consumer, p.config.Provider) switch p.specificationVersion { case models.V2: p.mockserver.WithSpecificationVersion(native.SPECIFICATION_VERSION_V2) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1a180daf9..e0b29f234 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting -## Output Logging +## Output Logging` Pact Go uses a simple log utility ([logutils](https://github.com/hashicorp/logutils)) to filter log messages. The CLI already contains flags to manage this, diff --git a/internal/native/message_server.go b/internal/native/message_server.go index 794355431..77348c631 100644 --- a/internal/native/message_server.go +++ b/internal/native/message_server.go @@ -8,42 +8,59 @@ typedef int bool; #define true 1 #define false 0 -typedef struct MessageHandle MessageHandle; -struct MessageHandle { +/// Wraps a Pact model struct +typedef struct InteractionHandle InteractionHandle; + +struct InteractionHandle { unsigned int interaction_ref; }; -/// Wraps a PactMessage model struct -typedef struct MessagePactHandle MessagePactHandle; -struct MessagePactHandle { - unsigned int pact_ref; +/// Wraps a Pact model struct +typedef struct PactHandle PactHandle; +struct PactHandle { + unsigned int pact_ref; }; -MessagePactHandle pactffi_new_message_pact(const char *consumer_name, const char *provider_name); -MessageHandle pactffi_new_message(MessagePactHandle pact, const char *description); -void pactffi_message_expects_to_receive(MessageHandle message, const char *description); -void pactffi_message_given(MessageHandle message, const char *description); -void pactffi_message_given_with_param(MessageHandle message, const char *description, const char *name, const char *value); -void pactffi_message_with_contents(MessageHandle message, const char *content_type, const char *body, int size); -void pactffi_message_with_metadata(MessageHandle message, const char *key, const char *value); -char* pactffi_message_reify(MessageHandle message); -int pactffi_write_message_pact_file(MessagePactHandle pact, const char *directory, bool overwrite); -void pactffi_with_message_pact_metadata(MessagePactHandle pact, const char *namespace, const char *name, const char *value); +PactHandle pactffi_new_message_pact(const char *consumer_name, const char *provider_name); +InteractionHandle pactffi_new_message(PactHandle pact, const char *description); +// Creates a new synchronous message interaction (request/response) and return a handle to it +InteractionHandle pactffi_new_sync_message_interaction(PactHandle pact, const char *description); +// Creates a new asynchronous message interaction (request/response) and return a handle to it +InteractionHandle pactffi_new_message_interaction(PactHandle pact, const char *description); +void pactffi_message_expects_to_receive(InteractionHandle message, const char *description); +void pactffi_message_given(InteractionHandle message, const char *description); +void pactffi_message_given_with_param(InteractionHandle message, const char *description, const char *name, const char *value); +void pactffi_message_with_contents(InteractionHandle message, const char *content_type, const char *body, int size); +void pactffi_message_with_metadata(InteractionHandle message, const char *key, const char *value); +char* pactffi_message_reify(InteractionHandle message); +int pactffi_write_message_pact_file(PactHandle pact, const char *directory, bool overwrite); +void pactffi_with_message_pact_metadata(PactHandle pact, const char *namespace, const char *name, const char *value); +int pactffi_write_pact_file(int mock_server_port, const char *directory, bool overwrite); + +int pactffi_using_plugin(PactHandle pact, const char *plugin_name, const char *plugin_version); +void pactffi_cleanup_plugins(PactHandle pact); +int pactffi_interaction_contents(InteractionHandle interaction, int interaction_part, const char *content_type, const char *contents); + +// Create a mock server for the provided Pact handle and transport. +int pactffi_create_mock_server_for_transport(PactHandle pact, const char *addr, int port, const char *transport, const char *transport_config); +bool pactffi_cleanup_mock_server(int mock_server_port); +char* pactffi_mock_server_mismatches(int mock_server_port); */ import "C" import ( + "encoding/json" "fmt" "log" "unsafe" ) type MessagePact struct { - handle C.MessagePactHandle + handle C.PactHandle } type Message struct { - handle C.MessageHandle + handle C.InteractionHandle } // MessageServer is the public interface for managing the message based interface @@ -77,12 +94,32 @@ func (m *MessageServer) WithMetadata(namespace, k, v string) *MessageServer { } // NewMessage initialises a new message for the current contract +// Deprecated: use NewAsyncMessageInteraction instead func (m *MessageServer) NewMessage() *Message { - cDescription := C.CString("") + // Alias + return m.NewAsyncMessageInteraction("") +} + +// NewSyncMessageInteraction initialises a new synchronous message interaction for the current contract +func (m *MessageServer) NewSyncMessageInteraction(description string) *Message { + cDescription := C.CString(description) + defer free(cDescription) + + i := &Message{ + handle: C.pactffi_new_sync_message_interaction(m.messagePact.handle, cDescription), + } + m.messages = append(m.messages, i) + + return i +} + +// NewAsyncMessageInteraction initialises a new asynchronous message interaction for the current contract +func (m *MessageServer) NewAsyncMessageInteraction(description string) *Message { + cDescription := C.CString(description) defer free(cDescription) i := &Message{ - handle: C.pactffi_new_message(m.messagePact.handle, cDescription), + handle: C.pactffi_new_message_interaction(m.messagePact.handle, cDescription), } m.messages = append(m.messages, i) @@ -165,6 +202,159 @@ func (i *Message) WithContents(contentType string, body []byte) *Message { return i } +// TODO: migrate plugin code to shared struct/code? + +// NewInteraction initialises a new interaction for the current contract +func (m *MessageServer) UsingPlugin(pluginName string, pluginVersion string) error { + cPluginName := C.CString(pluginName) + defer free(cPluginName) + cPluginVersion := C.CString(pluginVersion) + defer free(cPluginVersion) + + r := C.pactffi_using_plugin(m.messagePact.handle, cPluginName, cPluginVersion) + + // 1 - A general panic was caught. + // 2 - Failed to load the plugin. + // 3 - Pact Handle is not valid. + res := int(r) + switch res { + case 1: + return ErrPluginGenericPanic + case 2: + return ErrPluginFailed + case 3: + return ErrHandleNotFound + default: + if res != 0 { + return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code:", res) + } + } + + return nil +} + +// NewInteraction initialises a new interaction for the current contract +func (i *Message) WithPluginInteractionContents(interactionPart interactionType, contentType string, contents string) error { + cContentType := C.CString(contentType) + defer free(cContentType) + cContents := C.CString(contents) + defer free(cContents) + + r := C.pactffi_interaction_contents(i.handle, C.int(interactionPart), cContentType, cContents) + + // 1 - A general panic was caught. + // 2 - The mock server has already been started. + // 3 - The interaction handle is invalid. + // 4 - The content type is not valid. + // 5 - The contents JSON is not valid JSON. + // 6 - The plugin returned an error. + res := int(r) + switch res { + case 1: + return ErrPluginGenericPanic + case 2: + return ErrPluginMockServerStarted + case 3: + return ErrPluginInteractionHandleInvalid + case 4: + return ErrPluginInvalidContentType + case 5: + return ErrPluginInvalidJson + case 6: + return ErrPluginSpecificError + default: + if res != 0 { + return fmt.Errorf("an unknown error (code: %v) occurred when adding a plugin for the test. Received error code:", res) + } + } + + return nil +} + +// StartTransport starts up a mock server on the given address:port for the given transport +// https://docs.rs/pact_ffi/latest/pact_ffi/mock_server/fn.pactffi_create_mock_server_for_transport.html +func (m *MessageServer) StartTransport(transport string, address string, port int, config map[string][]interface{}) (int, error) { + if len(m.messages) == 0 { + return 0, ErrNoInteractions + } + + log.Println("[DEBUG] mock server starting on address:", address, port) + cAddress := C.CString(address) + defer free(cAddress) + + cTransport := C.CString(transport) + defer free(cTransport) + + configJson := stringFromInterface(config) + cConfig := C.CString(configJson) + defer free(cConfig) + + p := C.pactffi_create_mock_server_for_transport(m.messagePact.handle, cAddress, C.int(port), cTransport, cConfig) + + // | Error | Description + // |-------|------------- + // | -1 | An invalid handle was received. Handles should be created with pactffi_new_pact + // | -2 | transport_config is not valid JSON + // | -3 | The mock server could not be started + // | -4 | The method panicked + // | -5 | The address is not valid + msPort := int(p) + switch msPort { + case -1: + return 0, ErrInvalidMockServerConfig + case -2: + return 0, ErrInvalidMockServerConfig + case -3: + return 0, ErrMockServerUnableToStart + case -4: + return 0, ErrMockServerPanic + case -5: + return 0, ErrInvalidAddress + default: + if msPort > 0 { + log.Println("[DEBUG] mock server running on port:", msPort) + return msPort, nil + } + return msPort, fmt.Errorf("an unknown error (code: %v) occurred when starting a mock server for the test", msPort) + } +} + +// NewInteraction initialises a new interaction for the current contract +func (m *MessageServer) CleanupPlugins(pluginName string, pluginVersion string) { + C.pactffi_cleanup_plugins(m.messagePact.handle) +} + +// CleanupMockServer frees the memory from the previous mock server. +func (m *MessageServer) CleanupMockServer(port int) bool { + if len(m.messages) == 0 { + return true + } + log.Println("[DEBUG] mock server cleaning up port:", port) + res := C.pactffi_cleanup_mock_server(C.int(port)) + + return int(res) == 1 +} + +// MockServerMismatchedRequests returns a JSON object containing any mismatches from +// the last set of interactions. +func (m *MessageServer) MockServerMismatchedRequests(port int) []MismatchedRequest { + log.Println("[DEBUG] mock server determining mismatches:", port) + var res []MismatchedRequest + + mismatches := C.pactffi_mock_server_mismatches(C.int(port)) + // This method can return a nil pointer, in which case, it + // should be considered a failure (or at least, an issue) + // converting it to a string might also do nasty things here! + if mismatches == nil { + log.Println("[WARN] received a null pointer from the native interface, returning empty list of mismatches") + return []MismatchedRequest{} + } + + json.Unmarshal([]byte(C.GoString(mismatches)), &res) + + return res +} + func (i *Message) ReifyMessage() string { return C.GoString(C.pactffi_message_reify(i.handle)) } @@ -197,3 +387,34 @@ func (m *MessageServer) WritePactFile(dir string, overwrite bool) error { return fmt.Errorf("an unknown error ocurred when writing to pact file") } } + +// WritePactFile writes the Pact to file. +func (m *MessageServer) WritePactFileForServer(port int, dir string, overwrite bool) error { + log.Println("[DEBUG] writing pact file for message pact at dir:", dir) + cDir := C.CString(dir) + defer free(cDir) + + overwritePact := 0 + if overwrite { + overwritePact = 1 + } + + res := int(C.pactffi_write_pact_file(C.int(port), cDir, C.int(overwritePact))) + + /// | Error | Description | + /// |-------|-------------| + /// | 1 | The pact file was not able to be written | + /// | 2 | The message pact for the given handle was not found | + switch res { + case 0: + return nil + case 1: + return ErrMockServerPanic + case 2: + return ErrUnableToWritePactFile + case 3: + return ErrHandleNotFound + default: + return fmt.Errorf("an unknown error ocurred when writing to pact file") + } +} diff --git a/internal/native/message_server_test.go b/internal/native/message_server_test.go index 113f18bbe..fbb2cbd4d 100644 --- a/internal/native/message_server_test.go +++ b/internal/native/message_server_test.go @@ -3,13 +3,21 @@ package native import ( "bytes" "compress/gzip" + context "context" "encoding/base64" "encoding/json" + "fmt" "io/ioutil" - "log" + l "log" + "os" "testing" + "time" + + "github.com/pact-foundation/pact-go/v2/log" "github.com/stretchr/testify/assert" + grpc "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) func TestHandleBasedMessageTestsWithString(t *testing.T) { @@ -63,7 +71,7 @@ func TestHandleBasedMessageTestsWithJSON(t *testing.T) { }) body := m.ReifyMessage() - log.Println(body) // TODO: JSON is not stringified - probably should be? + l.Println(body) // TODO: JSON is not stringified - probably should be? var res jsonMessage err = json.Unmarshal([]byte(body), &res) @@ -147,3 +155,109 @@ type jsonMessage struct { Metadata map[string]string `json:"metadata"` Contents interface{} `json:"contents"` } + +func TestGrpcPluginInteraction(t *testing.T) { + tmpPactFolder, err := ioutil.TempDir("", "pact-go") + assert.NoError(t, err) + log.InitLogging() + log.SetLogLevel("TRACE") + + m := NewMessageServer("test-message-consumer", "test-message-provider") + // m := NewPact("test-grpc-consumer", "test-plugin-provider") + + // Protobuf plugin test + m.UsingPlugin("protobuf", "0.1.5") + // m.WithSpecificationVersion(SPECIFICATION_VERSION_V4) + + i := m.NewSyncMessageInteraction("grpc interaction") + + dir, _ := os.Getwd() + path := fmt.Sprintf("%s/plugin.proto", dir) + + grpcInteraction := `{ + "pact:proto": "` + path + `", + "pact:proto-service": "PactPlugin/InitPlugin", + "pact:content-type": "application/protobuf", + "request": { + "implementation": "notEmpty('pact-go-driver')", + "version": "matching(semver, '0.0.0')" + }, + "response": { + "catalogue": [ + { + "type": "INTERACTION", + "key": "test" + } + ] + } + }` + + i. + Given("plugin state"). + // For gRPC interactions we prpvide the config once for both the request and response parts + WithPluginInteractionContents(INTERACTION_PART_REQUEST, "application/protobuf", grpcInteraction) + + // Start the gRPC mock server + port, err := m.StartTransport("grpc", "127.0.0.1", 0, make(map[string][]interface{})) + assert.NoError(t, err) + defer m.CleanupMockServer(port) + + // Now we can make a normal gRPC request + initPluginRequest := &InitPluginRequest{ + Implementation: "pact-go-test", + Version: "1.0.0", + } + + // Need to make a gRPC call here + conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + l.Fatalf("did not connect: %v", err) + } + defer conn.Close() + c := NewPactPluginClient(conn) + + // Contact the server and print out its response. + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.InitPlugin(ctx, initPluginRequest) + if err != nil { + l.Fatalf("could not initialise the plugin: %v", err) + } + l.Printf("InitPluginResponse: %v", r) + + mismatches := m.MockServerMismatchedRequests(port) + if len(mismatches) != 0 { + assert.Len(t, mismatches, 0) + t.Log(mismatches) + } + + err = m.WritePactFileForServer(port, tmpPactFolder, true) + assert.NoError(t, err) +} + +// TODO: + +// In the test, you use `NewSyncMessageInteraction` but don't set a response type on the FFI, just the plugin. This is a little awkward +// Does this imply that a sync can be async? +// WithPluginInteractionContents(INTERACTION_PART_REQUEST, "application/protobuf", grpcInteraction) + +// // How to add metadata, binary contents ? + +// // -> https://docs.pact.io/slack/libpact_ffi-users.html#1646812690.612989 + +// Is the mental model to Ignore the V3 message pact stuff now (MessagePactHandle and MessageHandle) and just assume `PactHandle` and `InteractionHandle` are the main thinngs? +// If so, is it possible to get an updated view of which functions are essentially deprecated? I'm finding it very hard to know which methods to call. + +// For example, + +// MessagePactHandle PactHandle +// pactffi_with_message_pact_metadata --> pactffi_with_pact_metadata + +// MessageHandle --> InteractionHandle +// pactffi_with_message_pact_metadata --> pactffi_with_binary_file This would be confusing though + +// Example of methods on Interaction that don't make sense for all types. This means all client libraries need to know the cases in which it can/can't be used +// pactffi_response_status, pactffi_with_query_parameter, pactffi_with_request, pactffi_with_binary_file + +// Example of methods on MessageHandle that don't have equivalent on InteractionHandle +// pactffi_message_expects_to_receive, pactffi_message_with_contents, pactffi_message_reify diff --git a/internal/native/mock_server.go b/internal/native/mock_server.go index b326faebf..ecc1df838 100644 --- a/internal/native/mock_server.go +++ b/internal/native/mock_server.go @@ -125,9 +125,6 @@ int pactffi_with_multipart_file(InteractionHandle interaction, int interaction_p // https://docs.rs/pact_mock_server_ffi/0.0.7/pact_mock_server_ffi/fn.response_status.html void pactffi_response_status(InteractionHandle interaction, int status); -// Creates a new synchronous message interaction (request/response) and return a handle to it - InteractionHandle pactffi_new_sync_message_interaction(PactHandle pact, const char *description); - /// External interface to trigger a mock server to write out its pact file. This function should /// be called if all the consumer tests have passed. The directory to write the file to is passed /// as the second parameter. If a NULL pointer is passed, the current working directory is used. @@ -247,7 +244,7 @@ type MockServer struct { } // NewMockServer creates a new mock server for a given consumer/provider -func NewHTTPMockServer(consumer string, provider string) *MockServer { +func NewPact(consumer string, provider string) *MockServer { cConsumer := C.CString(consumer) cProvider := C.CString(provider) defer free(cConsumer) @@ -352,11 +349,18 @@ func (m *MockServer) CleanupMockServer(port int) bool { } // WritePactFile writes the Pact to file. +// TODO: expose overwrite func (m *MockServer) WritePactFile(port int, dir string) error { log.Println("[DEBUG] writing pact file for mock server on port:", port, ", dir:", dir) cDir := C.CString(dir) defer free(cDir) + // overwritePact := 0 + // if overwrite { + // overwritePact = 1 + // } + + // res := int(C.pactffi_write_pact_file(C.int(port), cDir, C.int(overwritePact))) res := int(C.pactffi_write_pact_file(C.int(port), cDir, C.int(0))) // | Error | Description | @@ -459,7 +463,7 @@ func (m *MockServer) StartTransport(transport string, address string, port int, log.Println("[DEBUG] mock server starting on address:", address, port) cAddress := C.CString(address) defer free(cAddress) - + cTransport := C.CString(transport) defer free(cTransport) @@ -469,7 +473,7 @@ func (m *MockServer) StartTransport(transport string, address string, port int, p := C.pactffi_create_mock_server_for_transport(m.pact.handle, cAddress, C.int(port), cTransport, cConfig) - // | Error | Description + // | Error | Description // |-------|------------- // | -1 | An invalid handle was received. Handles should be created with pactffi_new_pact // | -2 | transport_config is not valid JSON @@ -558,21 +562,7 @@ func (m *MockServer) NewInteraction(description string) *Interaction { return i } -// NewSyncMessageInteraction initialises a new synchronous message interaction for the current contract -func (m *MockServer) NewSyncMessageInteraction(description string) *Interaction { - cDescription := C.CString(description) - defer free(cDescription) - - i := &Interaction{ - handle: C.pactffi_new_sync_message_interaction(m.pact.handle, cDescription), - } - m.interactions = append(m.interactions, i) - - return i -} - // NewInteraction initialises a new interaction for the current contract -// TODO: why specify the name and version twice? func (i *Interaction) WithPluginInteractionContents(interactionPart interactionType, contentType string, contents string) error { cContentType := C.CString(contentType) defer free(cContentType) diff --git a/internal/native/mock_server_test.go b/internal/native/mock_server_test.go index 67e5b87c7..9ed5cdecb 100644 --- a/internal/native/mock_server_test.go +++ b/internal/native/mock_server_test.go @@ -6,15 +6,9 @@ import ( "net/http" "os" "testing" - "time" - - "context" - l "log" "github.com/pact-foundation/pact-go/v2/log" "github.com/stretchr/testify/assert" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/proto" ) @@ -135,7 +129,7 @@ func TestHandleBasedHTTPTests(t *testing.T) { tmpPactFolder, err := ioutil.TempDir("", "pact-go") assert.NoError(t, err) - m := NewHTTPMockServer("test-http-consumer", "test-http-provider") + m := NewPact("test-http-consumer", "test-http-provider") i := m.NewInteraction("some interaction") @@ -175,7 +169,7 @@ func TestPluginInteraction(t *testing.T) { assert.NoError(t, err) log.SetLogLevel("trace") - m := NewHTTPMockServer("test-plugin-consumer", "test-plugin-provider") + m := NewPact("test-plugin-consumer", "test-plugin-provider") // Protobuf plugin test m.UsingPlugin("protobuf", "0.0.3") @@ -296,81 +290,3 @@ var pactComplex = `{ } }] }` - -func TestGrpcPluginInteraction(t *testing.T) { - tmpPactFolder, err := ioutil.TempDir("", "pact-go") - assert.NoError(t, err) - log.InitLogging() - log.SetLogLevel("TRACE") - - m := NewHTTPMockServer("test-grpc-consumer", "test-plugin-provider") - - // Protobuf plugin test - m.UsingPlugin("protobuf", "0.1.5") - // m.WithSpecificationVersion(SPECIFICATION_VERSION_V4) - - i := m.NewSyncMessageInteraction("grpc interaction") - - dir, _ := os.Getwd() - path := fmt.Sprintf("%s/plugin.proto", dir) - - grpcInteraction := `{ - "pact:proto": "` + path + `", - "pact:proto-service": "PactPlugin/InitPlugin", - "pact:content-type": "application/protobuf", - "request": { - "implementation": "notEmpty('pact-go-driver')", - "version": "matching(semver, '0.0.0')" - }, - "response": { - "catalogue": [ - { - "type": "INTERACTION", - "key": "test" - } - ] - } - }` - - i. - Given("plugin state"). - // For gRPC interactions we prpvide the config once for both the request and response parts - WithPluginInteractionContents(INTERACTION_PART_REQUEST, "application/protobuf", grpcInteraction) - - // Start the gRPC mock server - port, err := m.StartTransport("grpc", "127.0.0.1", 0, make(map[string][]interface{})) - assert.NoError(t, err) - defer m.CleanupMockServer(port) - - // Now we can make a normal gRPC request - initPluginRequest := &InitPluginRequest{ - Implementation: "pact-go-test", - Version: "1.0.0", - } - - // Need to make a gRPC call here - conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - l.Fatalf("did not connect: %v", err) - } - defer conn.Close() - c := NewPactPluginClient(conn) - - // Contact the server and print out its response. - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - r, err := c.InitPlugin(ctx, initPluginRequest) - if err != nil { - l.Fatalf("could not initialise the plugin: %v", err) - } - l.Printf("InitPluginResponse: %v", r) - - mismatches := m.MockServerMismatchedRequests(port) - if len(mismatches) != 0 { - assert.Len(t, mismatches, 0) - t.Log(mismatches) - } - - err = m.WritePactFile(port, tmpPactFolder) - assert.NoError(t, err) -}