From 98aff6f55245e5e48a7cc73ef6237fbdfc9a2c89 Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Sat, 1 Aug 2020 13:22:53 +1000 Subject: [PATCH] wip --- Makefile | 2 +- command/version.go | 4 +- examples/v3/consumer_test.go | 32 +++-- v3/http.go | 58 ++++++--- v3/matcher.go | 26 ++-- v3/native/interface.go | 137 ++++++++++++++++++++ v3/native/mock_server_darwin.go | 116 ----------------- v3/native/mock_server_linux.go | 22 ++-- v3/native/mock_server_windows.go | 19 +++ v3/pact_file.go | 213 +++++++++++++++++++++++++++---- v3/pact_file_test.go | 12 +- 11 files changed, 438 insertions(+), 203 deletions(-) create mode 100644 v3/native/mock_server_windows.go diff --git a/Makefile b/Makefile index 19b64c720..5e89cf3f5 100755 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ bin: ls -hl build/ clean: - rm -rf build output dist + rm -rf build output dist examples/v3/pacts/* deps: snyk-install @echo "--- 🐿 Fetching build dependencies " diff --git a/command/version.go b/command/version.go index cb6232a8a..7169e7347 100644 --- a/command/version.go +++ b/command/version.go @@ -6,14 +6,14 @@ import ( "github.com/spf13/cobra" ) -var version = "v1.4.3" +var Version = "v1.4.3" var cliToolsVersion = "1.82.3" var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number of Pact Go", Long: `All software has versions. This is Pact Go's`, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Pact Go CLI %s, using CLI tools version %s", version, cliToolsVersion) + fmt.Printf("Pact Go CLI %s, using CLI tools version %s", Version, cliToolsVersion) }, } diff --git a/examples/v3/consumer_test.go b/examples/v3/consumer_test.go index 204d9532e..5f3bff910 100644 --- a/examples/v3/consumer_test.go +++ b/examples/v3/consumer_test.go @@ -13,6 +13,8 @@ import ( v3 "github.com/pact-foundation/pact-go/v3" ) +type s = v3.String + // Example Pact: How to run me! // 1. cd /examples // 2. go test -v -run TestConsumer @@ -20,19 +22,22 @@ func TestConsumer(t *testing.T) { type User struct { Name string `json:"name" pact:"example=billy"` LastName string `json:"lastName" pact:"example=sampson"` + Date string `json:"datetime" pact:"generator=time"` } // Create Pact connecting to local Daemon - pact := &v3.MockProvider{ + mockProvider := &v3.MockProvider{ Consumer: "MyConsumer", Provider: "MyProvider", Host: "localhost", + LogLevel: "DEBUG", } - defer pact.Teardown() + mockProvider.Setup() + defer mockProvider.Teardown() // Pass in test case var test = func(config v3.MockServerConfig) error { - u := fmt.Sprintf("http://localhost:%d/foobar", pact.ServerPort) + u := fmt.Sprintf("http://localhost:%d/foobar", mockProvider.ServerPort) req, err := http.NewRequest("GET", u, strings.NewReader(`{"name":"billy"}`)) // NOTE: by default, request bodies are expected to be sent with a Content-Type @@ -52,26 +57,31 @@ func TestConsumer(t *testing.T) { } // Set up our expected interactions. - pact. + mockProvider. AddInteraction(). Given("User foo exists"). UponReceiving("A request to get foo"). WithRequest(v3.Request{ Method: "GET", - Path: v3.String("/foobar"), - Headers: v3.MapMatcher{"Content-Type": v3.String("application/json"), "Authorization": v3.String("Bearer 1234")}, - Body: map[string]string{ - "name": "billy", + Path: s("/foobar"), + Headers: v3.MapMatcher{"Content-Type": s("application/json"), "Authorization": s("Bearer 1234")}, + Body: v3.MapMatcher{ + "name": s("billy"), }, }). WillRespondWith(v3.Response{ Status: 200, - Headers: v3.MapMatcher{"Content-Type": v3.String("application/json")}, - Body: v3.Match(&User{}), + Headers: v3.MapMatcher{"Content-Type": s("application/json")}, + // Body: v3.Match(&User{}), + Body: v3.MapMatcher{ + "dateTime": s("Bearer 1234"), + "name": s("Bearer 1234"), + "lastName": s("Bearer 1234"), + }, }) // Verify - if err := pact.Verify(test); err != nil { + if err := mockProvider.Verify(test); err != nil { log.Fatalf("Error on Verify: %v", err) } } diff --git a/v3/http.go b/v3/http.go index 480a2d460..d362be0d1 100644 --- a/v3/http.go +++ b/v3/http.go @@ -1,7 +1,5 @@ -/* -package v3 contains the main Pact DSL used in the Consumer -collaboration test cases, and Provider contract test verification. -*/ +//package v3 contains the main Pact DSL used in the Consumer +// collaboration test cases, and Provider contract test verification. package v3 // TODO: setup a proper state machine to prevent actions @@ -54,10 +52,6 @@ type MockProvider struct { // Defaults to `/pacts`. PactDir string `json:"-"` - // Specify which version of the Pact Specification should be used (1 or 2). - // Defaults to 2. - SpecificationVersion int `json:"pactSpecificationVersion,string"` - // Host is the address of the Mock and Verification Service runs on // Examples include 'localhost', '127.0.0.1', '[::1]' // Defaults to 'localhost' @@ -128,10 +122,6 @@ func (p *MockProvider) Setup() *MockProvider { p.PactDir = fmt.Sprintf(filepath.Join(dir, "pacts")) } - if p.SpecificationVersion == 0 { - p.SpecificationVersion = 2 - } - if p.ClientTimeout == 0 { p.ClientTimeout = 10 * time.Second } @@ -174,6 +164,8 @@ func (p *MockProvider) Setup() *MockProvider { // p.Server = p.pactClient.StartServer(args, port) // } + native.Init() + return p } @@ -195,9 +187,13 @@ func (p *MockProvider) setupLogging() { // Teardown stops the Pact Mock Server. This usually is called on completion // of each test suite. -func (p *MockProvider) Teardown() *MockProvider { +func (p *MockProvider) Teardown() error { log.Println("[DEBUG] teardown") if p.ServerPort != 0 { + err := native.WritePactFile(p.ServerPort, p.PactDir) + if err != nil { + return err + } if native.CleanupMockServer(p.ServerPort) { p.ServerPort = 0 @@ -205,7 +201,7 @@ func (p *MockProvider) Teardown() *MockProvider { log.Println("[DEBUG] unable to teardown server") } } - return p + return nil } // Verify runs the current test case against a Mock Service. @@ -215,13 +211,17 @@ func (p *MockProvider) Verify(integrationTest func(MockServerConfig) error) erro p.Setup() // Start server - fmt.Println("[DEBUG] Sending pact file:", formatJSONObject(p)) + serialisedPact := NewPactFile(p.Consumer, p.Provider, p.Interactions) + fmt.Println("[DEBUG] Sending pact file:", formatJSONObject(serialisedPact)) // TODO: wire this better - port := native.CreateMockServer(formatJSONObject(p), "0.0.0.0:0", false) + port := native.CreateMockServer(formatJSONObject(serialisedPact), "0.0.0.0:0", false) // TODO: not sure we want this? p.ServerPort = port + + // TODO: use cases for having server running post integration test? + // Probably not... defer native.CleanupMockServer(port) // Run the integration test @@ -238,15 +238,37 @@ func (p *MockProvider) Verify(integrationTest func(MockServerConfig) error) erro if !res { return fmt.Errorf("pact validation failed: %+v %+v", res, mismatches) } + p.WritePact() return nil } -func (p *MockProvider) displayMismatches(mismatches []native.Mismatch) { +// TODO: pretty print this to make it really easy to understand the problems +// See existing Pact/Ruby code examples +// What about the Rust/Elm compiler feedback, they are pretty great too. +func (p *MockProvider) displayMismatches(mismatches []native.MismatchedRequest) { if len(mismatches) > 0 { log.Println("[INFO] pact validation failed, errors: ") for _, m := range mismatches { - log.Println(m) + formattedRequest := fmt.Sprintf("%s %s", m.Request.Method, m.Request.Path) + switch m.Type { + case "missing-request": + fmt.Printf("Expected request to: %s, but did not receive one\n", formattedRequest) + case "request-not-found": + fmt.Printf("Unexpected request was received: %s\n", formattedRequest) + default: + // TODO: + } + + for _, detail := range m.Mismatches { + switch detail.Type { + case "HeaderMismatch": + fmt.Printf("Comparing Header: '%s'\n", detail.Key) + fmt.Println(detail.Mismatch) + fmt.Println("Expected:", detail.Expected) + fmt.Println("Actual:", detail.Actual) + } + } } } } diff --git a/v3/matcher.go b/v3/matcher.go index 214ead4e0..94cfc6a33 100644 --- a/v3/matcher.go +++ b/v3/matcher.go @@ -74,8 +74,8 @@ func (m eachLike) Type() MatcherClass { return ArrayMinLikeMatcher } -func (m eachLike) MatchingRule() matcherType { - matcher := matcherType{ +func (m eachLike) MatchingRule() ruleValue { + matcher := ruleValue{ "match": "type", } @@ -103,8 +103,8 @@ func (m like) Type() MatcherClass { return LikeMatcher } -func (m like) MatchingRule() matcherType { - return matcherType{ +func (m like) MatchingRule() ruleValue { + return ruleValue{ "match": "type", } } @@ -124,8 +124,8 @@ func (m term) Type() MatcherClass { return RegexMatcher } -func (m term) MatchingRule() matcherType { - return matcherType{ +func (m term) MatchingRule() ruleValue { + return ruleValue{ "match": "regex", "regex": m.Data.Matcher.Regex, } @@ -260,7 +260,7 @@ type Matcher interface { Type() MatcherClass // Generate the matching rule for this Matcher - MatchingRule() matcherType + MatchingRule() ruleValue } // MatcherClass is used to differentiate the various matchers when serialising @@ -297,8 +297,8 @@ func (s S) Type() MatcherClass { return LikeMatcher } -func (s S) MatchingRule() matcherType { - return matcherType{ +func (s S) MatchingRule() ruleValue { + return ruleValue{ "match": "type", } } @@ -323,8 +323,8 @@ func (s StructMatcher) Type() MatcherClass { return LikeMatcher } -func (s StructMatcher) MatchingRule() matcherType { - return matcherType{ +func (s StructMatcher) MatchingRule() ruleValue { + return ruleValue{ "match": "type", } } @@ -358,6 +358,7 @@ func objectToString(obj interface{}) string { // Supported Tag Formats // Minimum Slice Size: `pact:"min=2"` // String RegEx: `pact:"example=2000-01-01,regex=^\\d{4}-\\d{2}-\\d{2}$"` +// TODO: support generators func Match(src interface{}) Matcher { return match(reflect.TypeOf(src), getDefaults()) } @@ -507,3 +508,6 @@ func pluckParams(srcType reflect.Type, pactTag string) params { func triggerInvalidPactTagPanic(tag string, err error) { panic(fmt.Sprintf("match: encountered invalid pact tag %q . . . parsing failed with error: %v", tag, err)) } + +// Generators +// https://github.com/pact-foundation/pact-specification/tree/version-3#introduce-example-generators diff --git a/v3/native/interface.go b/v3/native/interface.go index c514c013c..bd95156e9 100644 --- a/v3/native/interface.go +++ b/v3/native/interface.go @@ -1,2 +1,139 @@ // Package native contains the c bindings into the Pact Reference types. package native + +/* +// Library headers +typedef int bool; +#define true 1 +#define false 0 + +void init(char* log); +int create_mock_server(char* pact, char* addr, bool tls); +int mock_server_matched(int port); +char* mock_server_mismatches(int port); +bool cleanup_mock_server(int port); +int write_pact_file(int port, char* dir); + +*/ +import "C" + +import ( + "C" + "encoding/json" + "fmt" + "log" +) + +// Request is the sub-struct of Mismatch +type Request struct { + Method string `json:"method"` + Path string `json:"path"` + Query string `json:"query,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Body interface{} `json:"body,omitempty"` +} + +// Mismatch is a type returned from the validation process +// [ +// { +// "method": "GET", +// "mismatches": [ +// { +// "actual": "", +// "expected": "\"Bearer 1234\"", +// "key": "Authorization", +// "mismatch": "Expected header 'Authorization' but was missing", +// "type": "HeaderMismatch" +// } +// ], +// "path": "/foobar", +// "type": "request-mismatch" +// } +// ] +type MismatchDetail struct { + Actual string + Expected string + Key string + Mismatch string + Type string +} +type MismatchedRequest struct { + Request + Mismatches []MismatchDetail + Type string +} + +// Init initialises the library +func Init() { + log.Println("[DEBUG] initialising framework") + C.init(C.CString("LOG_LEVEL")) +} + +// CreateMockServer creates a new Mock Server from a given Pact file. +func CreateMockServer(pact string, address string, tls bool) int { + log.Println("[DEBUG] mock server starting") + res := C.create_mock_server(C.CString(pact), C.CString(address), 0) + log.Println("[DEBUG] mock server running on port:", res) + return int(res) +} + +// Verify verifies that all interactions were successful. If not, returns a slice +// of Mismatch-es. Does not write the pact or cleanup server. +func Verify(port int, dir string) (bool, []MismatchedRequest) { + res := C.mock_server_matched(C.int(port)) + + mismatches := MockServerMismatchedRequests(port) + log.Println("[DEBUG] mock server mismatches:", len(mismatches)) + + return int(res) == 1, mismatches +} + +// MockServerMismatchedRequests returns a JSON object containing any mismatches from +// the last set of interactions. +func MockServerMismatchedRequests(port int) []MismatchedRequest { + log.Println("[DEBUG] mock server determining mismatches:", port) + var res []MismatchedRequest + + mismatches := C.mock_server_mismatches(C.int(port)) + json.Unmarshal([]byte(C.GoString(mismatches)), &res) + + return res +} + +// CleanupMockServer frees the memory from the previous mock server. +func CleanupMockServer(port int) bool { + log.Println("[DEBUG] mock server cleaning up port:", port) + res := C.cleanup_mock_server(C.int(port)) + + return int(res) == 1 +} + +var ( + // ErrMockServerPanic indicates a panic ocurred when invoking the remote Mock Server. + ErrMockServerPanic = fmt.Errorf("a general panic occured when invoking mock service") + + // ErrUnableToWritePactFile indicates an error when writing the pact file to disk. + ErrUnableToWritePactFile = fmt.Errorf("unable to write to file") + + // ErrMockServerNotfound indicates the Mock Server could not be found. + ErrMockServerNotfound = fmt.Errorf("unable to find mock server with the given port") +) + +// WritePactFile writes the Pact to file. +func WritePactFile(port int, dir string) error { + log.Println("[DEBUG] pact verify on port:", port, ", dir:", dir) + res := int(C.write_pact_file(C.int(port), C.CString(dir))) + + switch res { + case 0: + return nil + case 1: + return ErrMockServerPanic + case 2: + return ErrUnableToWritePactFile + case 3: + return ErrMockServerNotfound + default: + return fmt.Errorf("an unknown error ocurred when writing to pact file") + } +} diff --git a/v3/native/mock_server_darwin.go b/v3/native/mock_server_darwin.go index f790ab1f6..9ade9b0e5 100644 --- a/v3/native/mock_server_darwin.go +++ b/v3/native/mock_server_darwin.go @@ -17,119 +17,3 @@ int write_pact_file(int port, char* dir); */ import "C" -import ( - "encoding/json" - "fmt" - "log" -) - -// Request is the sub-struct of Mismatch -type Request struct { - Method string `json:"method"` - Path string `json:"path"` - Query string `json:"query,omitempty"` - Headers map[string]string `json:"headers,omitempty"` - Body interface{} `json:"body,omitempty"` -} - -// Mismatch is a type returned from the validation process -// -// [ -// { -// "method": "GET", -// "path": "/", -// "request": { -// "body": { -// "pass": 1234, -// "user": { -// "address": "some address", -// "name": "someusername", -// "phone": 12345678, -// "plaintext": "plaintext" -// } -// }, -// "method": "GET", -// "path": "/" -// }, -// "type": "missing-request" -// } -// ] -type Mismatch struct { - Request Request - Type string -} - -// Init initialises the library -func Init() { - log.Println("[DEBUG] initialising framework") - C.init(C.CString("")) -} - -// CreateMockServer creates a new Mock Server from a given Pact file. -func CreateMockServer(pact string, address string, tls bool) int { - log.Println("[DEBUG] mock server starting") - res := C.create_mock_server(C.CString(pact), C.CString(address), 0) - log.Println("[DEBUG] mock server running on port:", res) - return int(res) -} - -// Verify verifies that all interactions were successful. If not, returns a slice -// of Mismatch-es. Does not write the pact or cleanup server. -func Verify(port int, dir string) (bool, []Mismatch) { - res := C.mock_server_matched(C.int(port)) - - mismatches := MockServerMismatches(port) - log.Println("[DEBUG] mock server mismatches:", len(mismatches)) - - return int(res) == 1, mismatches -} - -// MockServerMismatches returns a JSON object containing any mismatches from -// the last set of interactions. -func MockServerMismatches(port int) []Mismatch { - log.Println("[DEBUG] mock server determining mismatches:", port) - var res []Mismatch - - mismatches := C.mock_server_mismatches(C.int(port)) - json.Unmarshal([]byte(C.GoString(mismatches)), &res) - - return res -} - -// CleanupMockServer frees the memory from the previous mock server. -func CleanupMockServer(port int) bool { - log.Println("[DEBUG] mock server cleaning up port:", port) - res := C.cleanup_mock_server(C.int(port)) - - return int(res) == 1 -} - -var ( - // ErrMockServerPanic indicates a panic ocurred when invoking the remote Mock Server. - ErrMockServerPanic = fmt.Errorf("a general panic occured when invoking mock service") - - // ErrUnableToWritePactFile indicates an error when writing the pact file to disk. - ErrUnableToWritePactFile = fmt.Errorf("unable to write to file") - - // ErrMockServerNotfound indicates the Mock Server could not be found. - ErrMockServerNotfound = fmt.Errorf("unable to find mock server with the given port") -) - -// WritePactFile writes the Pact to file. -func WritePactFile(port int, dir string) error { - log.Println("[DEBUG] pact verify on port:", port, ", dir:", dir) - res := int(C.write_pact_file(C.int(port), C.CString(dir))) - - switch res { - case 0: - return nil - case 1: - return ErrMockServerPanic - case 2: - return ErrUnableToWritePactFile - case 3: - return ErrMockServerNotfound - default: - return fmt.Errorf("an unknown error ocurred when writing to pact file") - } -} diff --git a/v3/native/mock_server_linux.go b/v3/native/mock_server_linux.go index 0a0c3f9ca..ee7963c4a 100644 --- a/v3/native/mock_server_linux.go +++ b/v3/native/mock_server_linux.go @@ -1,17 +1,19 @@ package native /* -#cgo LDFLAGS: ${SRCDIR}/../../libs/libpact_mock_server.dylib +#cgo LDFLAGS: ${SRCDIR}/../../libs/libpact_mock_server_ffi.so // Library headers -int create_mock_server(char* pact, int port); +typedef int bool; +#define true 1 +#define false 0 + +void init(char* log); +int create_mock_server(char* pact, char* addr, bool tls); +int mock_server_matched(int port); +char* mock_server_mismatches(int port); +bool cleanup_mock_server(int port); +int write_pact_file(int port, char* dir); + */ import "C" -import "fmt" - -// CreateMockServer creates a new Mock Server from a given Pact file -func CreateMockServer(pact string) int { - res := C.create_mock_server(C.CString(pact), 0) - fmt.Println("Mock Server running on port:", res) - return int(res) -} diff --git a/v3/native/mock_server_windows.go b/v3/native/mock_server_windows.go new file mode 100644 index 000000000..d85b6b4c8 --- /dev/null +++ b/v3/native/mock_server_windows.go @@ -0,0 +1,19 @@ +package native + +/* +#cgo LDFLAGS: ${SRCDIR}/../../libs/libpact_mock_server_ffi.dll + +// Library headers +typedef int bool; +#define true 1 +#define false 0 + +void init(char* log); +int create_mock_server(char* pact, char* addr, bool tls); +int mock_server_matched(int port); +char* mock_server_mismatches(int port); +bool cleanup_mock_server(int port); +int write_pact_file(int port, char* dir); + +*/ +import "C" diff --git a/v3/pact_file.go b/v3/pact_file.go index c1a31a096..30a3927ca 100644 --- a/v3/pact_file.go +++ b/v3/pact_file.go @@ -5,6 +5,8 @@ import ( "log" "reflect" "strconv" + + version "github.com/pact-foundation/pact-go/command" ) // Example matching rule / generated doc @@ -33,33 +35,169 @@ import ( // } // } -// matcherType is essentially a key value JSON pairs for serialisation -type matcherType map[string]interface{} +// ruleValue is essentially a key value JSON pairs for serialisation +// TODO: this is actually more typed than this +// once we understand the model better, let's make it more type-safe +type ruleValue map[string]interface{} // Matching Rule -type matchingRuleType map[string]matcherType +type rule struct { + Body ruleValue `json:"body,omitempty"` + Headers ruleValue `json:"headers,omitempty"` + Query ruleValue `json:"query,omitempty"` + Path ruleValue `json:"path,omitempty"` +} + +type matchingRule = rule +type generator = rule + +var pactGoMetadata = map[string]interface{}{ + "pactGo": map[string]string{ + "version": version.Version, + }, + "pactSpecification": map[string]interface{}{ + "version": "2.0.0", + }, +} + +// type pactFileBody map[string]interface{} + +type pactRequest struct { + Method string `json:"method"` + Path Matcher `json:"path"` + Query MapMatcher `json:"query,omitempty"` + Headers MapMatcher `json:"headers,omitempty"` + Body interface{} `json:"body"` + MatchingRules map[string]interface{} `json:"matchingRules"` + MatchingRules2 matchingRule `json:"matchingRules2,omitempty"` + Generators generator `json:"generators"` +} + +type pactResponse struct { + Status int `json:"status"` + Headers MapMatcher `json:"headers,omitempty"` + Body interface{} `json:"body,omitempty"` + MatchingRules map[string]interface{} `json:"matchingRules"` + MatchingRules2 matchingRule `json:"matchingRules2,omitempty"` + Generators generator `json:"generators"` +} + +type pactInteraction struct { + Description string `json:"description"` + State string `json:"providerState,omitempty"` + Request pactRequest `json:"request"` + Response pactResponse `json:"response"` +} -// PactFileBody is what will be serialised to the Pactfile in the request body examples and matching rules +// pactFile is what will be serialised to the Pactfile in the request body examples and matching rules // given a structure containing matchers. -// TODO: any matching rules will need to be merged with other aspects (e.g. headers, path). +// TODO: any matching rules will need to be merged with other aspects (e.g. headers, path, query). // ...still very much spike/POC code -type PactFileBody struct { - // Matching rules used by the verifier to confirm Provider confirms to Pact. - MatchingRules matchingRuleType `json:"matchingRules"` +type pactFile struct { + // Consumer is the name of the Consumer/Client. + Consumer string `json:"consumer"` + + // Provider is the name of the Providing service. + Provider string `json:"provider"` + + // SpecificationVersion is the version of the Pact Spec this implementation supports + SpecificationVersion int `json:"pactSpecificationVersion,string"` + + interactions []*Interaction + + // Interactions are all of the request/response expectations, with matching rules and generators + Interactions []pactInteraction `json:"interactions"` + + Metadata map[string]interface{} `json:"metadata"` +} + +func pactInteractionFromInteraction(interaction Interaction) pactInteraction { + return pactInteraction{ + Description: interaction.Description, + State: interaction.State, + Request: pactRequest{ + Method: interaction.Request.Method, + Body: interaction.Request.Body, + Headers: interaction.Request.Headers, + Query: interaction.Request.Query, + Path: interaction.Request.Path, + // Generators: make(generatorType), + MatchingRules: make(ruleValue), + MatchingRules2: generator{ + Body: make(ruleValue), + Headers: make(ruleValue), + Path: make(ruleValue), + Query: make(ruleValue), + }, + }, + Response: pactResponse{ + Status: interaction.Response.Status, + Body: interaction.Response.Body, + Headers: interaction.Response.Headers, + // Generators: make(generatorType), + MatchingRules: make(ruleValue), + MatchingRules2: matchingRule{ + Body: make(ruleValue), + Headers: make(ruleValue), + Path: make(ruleValue), + Query: make(ruleValue), + }, + }, + } +} + +func NewPactFile(Consumer string, Provider string, interactions []*Interaction) pactFile { + p := pactFile{ + Interactions: make([]pactInteraction, 0), + interactions: interactions, + Metadata: pactGoMetadata, + Consumer: Consumer, + Provider: Provider, + SpecificationVersion: 2, + } + p.generatePactFile() - // Generated test body for the consumer testing via the Mock Server. - Body map[string]interface{} `json:"body"` + return p } -// PactBodyBuilder takes a map containing recursive Matchers and generates the rules +func (p *pactFile) generatePactFile() *pactFile { + for _, interaction := range p.interactions { + fmt.Printf("Serialising interaction: %+v \n", *interaction) + serialisedInteraction := pactInteractionFromInteraction(*interaction) + + // TODO: this is just the request body, need the same for response! + _, _, requestBodyMatchingRules, _ := buildPactBody("", interaction.Request.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) + _, _, responseBodyMatchingRules, _ := buildPactBody("", interaction.Response.Body, make(map[string]interface{}), "$.body", make(ruleValue), make(ruleValue)) + + // v2 + serialisedInteraction.Request.MatchingRules = requestBodyMatchingRules + serialisedInteraction.Response.MatchingRules = responseBodyMatchingRules + + // v3 only + // serialisedInteraction.Request.MatchingRules.Body = requestBodyMatchingRules + // serialisedInteraction.Response.MatchingRules.Body = responseBodyMatchingRules + + // TODO + buildPactHeaders() + buildPactQuery() + buildPactPath() + + fmt.Printf("appending interaction: %+v \n", serialisedInteraction) + p.Interactions = append(p.Interactions, serialisedInteraction) + } + + return p +} + +// pactBodyBuilder takes a map containing recursive Matchers and generates the rules // to be serialised into the Pact file. -func PactBodyBuilder(root map[string]interface{}) PactFileBody { - pactFile := PactFileBody{} - fmt.Printf("root: %+v", root) - _, pactFile.Body, pactFile.MatchingRules = build("", root, make(map[string]interface{}), - "$.body", make(matchingRuleType)) +func pactBodyBuilder(root map[string]interface{}) pactFile { + // Generators: make(generatorType), + // MatchingRules: make(matchingRuleType), + // Metadata: pactGoMetadata, + // return file + return pactFile{} - return pactFile } const pathSep = "." @@ -67,7 +205,7 @@ const allListItems = "[*]" const startList = "[" const endList = "]" -// Recurse the Matcher tree and build up an example body and set of matchers for +// Recurse the Matcher tree and buildPactBody up an example body and set of matchers for // the Pact file. Ideally this stays as a pure function, but probably might need // to store matchers externally. // @@ -76,11 +214,11 @@ const endList = "]" // Arguments: // - key => Current key in the body to set // - value => Value for the current key, may be a primitive, object or another Matcher -// - body => Current state of the body map (body will be the returned Pact body for serialisation) +// - body => Current state of the body map to be built up (body will be the returned Pact body for serialisation) // - path => Path to the current key // - matchingRules => Current set of matching rules (matching rules will also be serialised into the Pact) -func build(key string, value interface{}, body map[string]interface{}, path string, - matchingRules matchingRuleType) (string, map[string]interface{}, matchingRuleType) { +func buildPactBody(key string, value interface{}, body map[string]interface{}, path string, + matchingRules ruleValue, generators ruleValue) (string, map[string]interface{}, ruleValue, ruleValue) { log.Println("[DEBUG] dsl generator: recursing => key:", key, ", body:", body, ", value: ", value) switch t := value.(type) { @@ -88,7 +226,6 @@ func build(key string, value interface{}, body map[string]interface{}, path stri case Matcher: switch t.Type() { - // ArrayLike Matchers case ArrayMinLikeMatcher, ArrayMaxLikeMatcher: fmt.Println("Array matcher") times := 1 @@ -104,7 +241,7 @@ func build(key string, value interface{}, body map[string]interface{}, path stri minArray := make([]interface{}, times) // TODO: why does this exist? -> Umm, it's what recurses the array item values! - build("0", t.GetValue(), arrayMap, path+buildPath(key, allListItems), matchingRules) + buildPactBody("0", t.GetValue(), arrayMap, path+buildPath(key, allListItems), matchingRules, generators) log.Println("[DEBUG] dsl generator: adding matcher (arrayLike) =>", path+buildPath(key, "")) matchingRules[path+buildPath(key, "")] = m.MatchingRule() @@ -122,7 +259,6 @@ func build(key string, value interface{}, body map[string]interface{}, path stri fmt.Printf("Updating body: %+v, minArray: %+v", body, minArray) path = path + buildPath(key, "") - // Simple Matchers (Terminal cases) case RegexMatcher, LikeMatcher: fmt.Println("regex matcher") body[key] = t.GetValue() @@ -141,7 +277,7 @@ func build(key string, value interface{}, body map[string]interface{}, path stri // I also had to do it for the Array*LikeMatcher's, which I also don't like for i, el := range t { k := fmt.Sprintf("%d", i) - build(k, el, arrayMap, path+buildPath(key, fmt.Sprintf("%s%d%s", startList, i, endList)), matchingRules) + buildPactBody(k, el, arrayMap, path+buildPath(key, fmt.Sprintf("%s%d%s", startList, i, endList)), matchingRules, generators) arrayValues[i] = arrayMap[k] } body[key] = arrayValues @@ -156,9 +292,26 @@ func build(key string, value interface{}, body map[string]interface{}, path stri // Starting position if key == "" { - _, body, matchingRules = build(k, v, copyMap(body), path, matchingRules) + _, body, matchingRules, generators = buildPactBody(k, v, copyMap(body), path, matchingRules, generators) + } else { + _, body[key], matchingRules, generators = buildPactBody(k, v, entry, path, matchingRules, generators) + } + } + + // Specialised case of map (above) + // TODO: DRY this? + case MapMatcher: + entry := make(map[string]interface{}) + path = path + buildPath(key, "") + + for k, v := range t { + log.Println("[DEBUG] dsl generator => map type. recursing into key =>", k) + + // Starting position + if key == "" { + _, body, matchingRules, generators = buildPactBody(k, v, copyMap(body), path, matchingRules, generators) } else { - _, body[key], matchingRules = build(k, v, entry, path, matchingRules) + _, body[key], matchingRules, generators = buildPactBody(k, v, entry, path, matchingRules, generators) } } @@ -171,9 +324,13 @@ func build(key string, value interface{}, body map[string]interface{}, path stri log.Println("[DEBUG] dsl generator => returning body: ", body) - return path, body, matchingRules + return path, body, matchingRules, generators } +func buildPactHeaders() {} +func buildPactPath() {} +func buildPactQuery() {} + // TODO: allow regex in request paths. func buildPath(name string, children string) string { // We know if a key is an integer, it's not valid JSON and therefore is Probably diff --git a/v3/pact_file_test.go b/v3/pact_file_test.go index b1cebb5e1..7a42fc6cc 100644 --- a/v3/pact_file_test.go +++ b/v3/pact_file_test.go @@ -13,13 +13,13 @@ func TestPactFile_term(t *testing.T) { expectedBody := formatJSON(`{ "id": 127 }`) - expectedMatchingRules := matchingRuleType{ + expectedMatchingRules := matchingRule{ "$.body.id": map[string]interface{}{ "match": "type", }, } - body := PactBodyBuilder(matcher) + body := pactBodyBuilder(matcher) result := formatJSONObject(body.Body) if expectedBody != result { @@ -42,14 +42,14 @@ func TestPactFile_ArrayMinLike(t *testing.T) { 27 ] }`) - expectedMatchingRules := matchingRuleType{ + expectedMatchingRules := matchingRule{ "$.body.users": map[string]interface{}{ "match": "type", "min": 3, }, } - body := PactBodyBuilder(matcher) + body := pactBodyBuilder(matcher) result := formatJSONObject(body.Body) if expectedBody != result { @@ -78,7 +78,7 @@ func TestPactFile_ArrayMinLikeWithNested(t *testing.T) { } ] }`) - expectedMatchingRules := matchingRuleType{ + expectedMatchingRules := matchingRule{ "$.body.users": map[string]interface{}{ "match": "type", "min": 3, @@ -89,7 +89,7 @@ func TestPactFile_ArrayMinLikeWithNested(t *testing.T) { }, } - body := PactBodyBuilder(matcher) + body := generatePactFile(matcher) result := formatJSONObject(body.Body) if expectedBody != result {