diff --git a/client/message_service.go b/client/message_service.go new file mode 100644 index 000000000..9dd50f09b --- /dev/null +++ b/client/message_service.go @@ -0,0 +1,24 @@ +package client + +import ( + "log" +) + +// MessageService is a wrapper for the Pact Message service. +type MessageService struct { + ServiceManager +} + +// NewService creates a new MessageService with default settings. +// Named Arguments allowed: +// --consumer +// --provider +// --pact-dir +func (v *MessageService) NewService(args []string) Service { + v.Args = args + + log.Printf("[DEBUG] starting message service with args: %v\n", v.Args) + v.Cmd = "pact-message" + + return v +} diff --git a/client/message_verification_service.go b/client/message_verification_service.go deleted file mode 100644 index 1cc53cbcf..000000000 --- a/client/message_verification_service.go +++ /dev/null @@ -1,26 +0,0 @@ -package client - -import ( - "log" -) - -// MessageVerificationService is a wrapper for the Pact Provider Verifier Service. -type MessageVerificationService struct { - ServiceManager -} - -// NewService creates a new MessageVerificationService with default settings. -// Named Arguments allowed: -// --consumer -// --provider -// --pact-dir -func (v *MessageVerificationService) NewService(args []string) Service { - v.Args = args - // Currently has an issue, see https://travis-ci.org/pact-foundation/pact-message-ruby/builds/357675751 - // v.Args = []string{"update", `{ "description": "a test mesage", "content": { "name": "Mary" } }`, "--consumer", "from", "--provider", "golang", "--pact-dir", "/tmp"} - - log.Printf("[DEBUG] starting message service with args: %v\n", v.Args) - v.Cmd = "pact-message" - - return v -} diff --git a/dsl/client.go b/dsl/client.go index 3746edf03..1b18b2e54 100644 --- a/dsl/client.go +++ b/dsl/client.go @@ -53,7 +53,7 @@ func newClient(MockServiceManager client.Service, verificationServiceManager cli // NewClient creates a new Pact client manager with defaults func NewClient() *PactClient { - return newClient(&client.MockService{}, &client.VerificationService{}, &client.MessageVerificationService{}) + return newClient(&client.MockService{}, &client.VerificationService{}, &client.MessageService{}) } // StartServer starts a remote Pact Mock Server. @@ -217,6 +217,64 @@ func (p *PactClient) UpdateMessagePact(request types.PactMessageRequest) error { return fmt.Errorf("error creating message: %s\n\nSTDERR:\n%s\n\nSTDOUT:\n%s", err, stdErr, stdOut) } +// ReifyMessage takes a structured object, potentially containing nested Matchers +// and returns an object with just the example (generated) content +// The object may be a simple JSON primitive e.g. string or number or a complex object +func (p *PactClient) ReifyMessage(request types.PactReificationRequest) (response interface{}, err error) { + log.Println("[DEBUG] client: adding pact message...") + + // Convert request into flags, and validate request + err = request.Validate() + if err != nil { + return + } + + svc := p.messageSvcManager.NewService(request.Args) + cmd := svc.Command() + + stdOutPipe, err := cmd.StdoutPipe() + if err != nil { + return + } + stdErrPipe, err := cmd.StderrPipe() + if err != nil { + return + } + err = cmd.Start() + if err != nil { + return + } + stdOut, err := ioutil.ReadAll(stdOutPipe) + if err != nil { + return + } + stdErr, err := ioutil.ReadAll(stdErrPipe) + if err != nil { + return + } + + err = cmd.Wait() + + decoder := json.NewDecoder(bytes.NewReader(stdOut)) + + dErr := decoder.Decode(&response) + if dErr == nil { + return + } + + if err == nil { + err = dErr + } + + if err == nil { + return + } + + err = fmt.Errorf("error creating message: %s\n\nSTDERR:\n%s\n\nSTDOUT:\n%s", err, stdErr, stdOut) + + return +} + // Get a port given a URL func getPort(rawURL string) int { parsedURL, err := url.Parse(rawURL) diff --git a/dsl/matcher.go b/dsl/matcher.go index 2ae5577f4..164a77cd8 100644 --- a/dsl/matcher.go +++ b/dsl/matcher.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "log" - "reflect" "strconv" "time" ) @@ -186,18 +185,15 @@ func (m Matcher) isMatcher() {} // GetValue returns the raw generated value for the matcher // without any of the matching detail context func (m Matcher) GetValue() interface{} { - log.Println("GETTING VALUE!") class, ok := m["json_class"] if !ok { - log.Println("GETTING VALUE!- NOT OK") return nil } // extract out the value switch class { case "Pact::ArrayLike": - log.Println("GETTING VALUE!- ARRAY") contents := m["contents"] min, err := strconv.Atoi(fmt.Sprintf("%d", m["min"])) if err != nil { @@ -212,14 +208,11 @@ func (m Matcher) GetValue() interface{} { return data case "Pact::SomethingLike": - log.Println("GETTING VALUE!- something like") return m["contents"] case "Pact::Term": - log.Println("GETTING VALUE!- term") data := m["data"].(map[string]interface{}) return data["generate"] } - log.Println("GETTING VALUE!- MEH?!") return nil } @@ -295,151 +288,6 @@ func getMatcher(obj interface{}) (Matcher, bool) { return nil, false } -var loop int - -func extractPayload(obj interface{}) interface{} { - fmt.Println("extractpaload") - loop = 0 - - // special case: top level matching object - // we need to strip the properties - stack := make(map[string]interface{}) - - // Convert to and from JSON to get a map[string]interface{} - data, err := json.Marshal(obj) - if err != nil { - return nil - } - - // var newObj map[string]interface{} - var newObj interface{} - json.Unmarshal(data, &newObj) - - // matcher, ok := getMatcher(obj) - // if ok { - // fmt.Println("top level matcher", matcher, "returning value:", getMatcherValue(matcher)) - // return extractPayloadRecursive(getMatcherValue(matcher), stack) - // } - - // fmt.Println("not a top level matcher, returning value:", obj) - return extractPayloadRecursive(newObj, stack) -} - -// Recurse the object removing any underlying matching guff, returning -// the raw example content (ready for JSON marshalling) -// NOTE: type information is going to be lost here which is OK -// because it must be mapped to JSON encodable types -// It is expected that any object is marshalled to JSON and into a map[string]interface{} -// for use here -// It will probably break custom, user-supplied types? e.g. a User{} or ShoppingCart{}? -// But then any enclosed Matchers will likely break them anyway -func extractPayloadRecursive(obj interface{}, stack interface{}) interface{} { - loop = loop + 1 - if loop > 10 { - log.Println("oh oh, non terminating - bail!") - return nil - } - original := reflect.ValueOf(obj) - - fmt.Println("------------------------------") - fmt.Println("extracting payload recursively") - fmt.Printf("obj: %+v\n", obj) - fmt.Printf("Stack: %+v\n", stack) - - // switch obj.(type) - switch original.Kind() { - // The first cases handle nested structures and translate them recursively - - // If it is a pointer we need to unwrap and call once again - case reflect.Ptr: - log.Println("[DEBUG] Pointer") - // To get the actual value of the original we have to call Elem() - // At the same time this unwraps the pointer so we don't end up in - // an infinite recursion - originalValue := original.Elem() - - // Check if the pointer is nil - if !originalValue.IsValid() { - log.Println("[WARN] pointer not properly unmarshalled") - return nil - } - - // Unwrap the newly created pointer - extractPayloadRecursive(originalValue, stack) - - // If it is an interface (which is very similar to a pointer), do basically the - // same as for the pointer. Though a pointer is not the same as an interface so - // note that we have to call Elem() after creating a new object because otherwise - // we would end up with an actual pointer - case reflect.Interface: - log.Println("[DEBUG] Interface") - - // Get rid of the wrapping interface - originalValue := original.Elem() - - // Create a new object. Now new gives us a pointer, but we want the value it - // points to, so we have to call Elem() to unwrap it - copyValue := reflect.New(originalValue.Type()).Elem() - extractPayloadRecursive(copyValue, stack) - - // If it is a struct we translate each field - // case reflect.Struct: - // log.Println("[DEBUG] Struct") - // _, ok := getMatcher(obj) - // if ok { - // fmt.Println("2. MATCHER!") - // } - - // for i := 0; i < original.NumField(); i++ { - // extractPayloadRecursive(original.Field(i), stack) - // } - - // If it is a slice we create a new slice and translate each element - case reflect.Slice: - log.Println("[DEBUG] Slice") - for i := 0; i < original.Len(); i++ { - extractPayloadRecursive(original.Index(i).Interface(), stack) - } - - // If it is a map we create a new map and translate each value - case reflect.Map: - log.Println("[DEBUG] Map") - stackMap, ok := stack.(map[string]interface{}) - - if !ok { - log.Println("STACK is not a map[]") - stack = make(map[string]interface{}) - stackMap, _ = stack.(map[string]interface{}) - } - - for k, v := range obj.(map[string]interface{}) { - matcher, ok := getMatcher(v) - fmt.Println(k, "=>", v) - if ok { - value := matcher.GetValue() - fmt.Println("3. Map is a MATCHER!", value) - stackMap[k] = value - extractPayloadRecursive(value, stackMap[k]) - } else { - stackMap[k] = v - extractPayloadRecursive(v, stackMap[k]) - } - } - - // If it is a string translate it (yay finally we're doing what we came for) - case reflect.String: - fmt.Println("STRING") - return obj - // copy.SetString(original.Interface().(string)) - - // And everything else will simply be taken from the original - default: - fmt.Println("something else") - } - - return stack -} - // MapMatcher allows a map[string]string-like object // to also contain complex matchers type MapMatcher map[string]StringMatcher diff --git a/dsl/matcher_test.go b/dsl/matcher_test.go index 0bd4d144d..f87809c86 100644 --- a/dsl/matcher_test.go +++ b/dsl/matcher_test.go @@ -8,8 +8,6 @@ import ( "reflect" "regexp" "testing" - - "github.com/google/go-cmp/cmp" ) func TestMatcher_TermString(t *testing.T) { @@ -464,74 +462,6 @@ func TestMatcher_SugarMatchers(t *testing.T) { } } -func TestMatcher_extractPayloadTopLevelMatcher(t *testing.T) { - m := Matcher{ - "json_class": "Pact::SomethingLike", - "contents": "something", - } - if extractPayload(m) != "something" { - t.Fatal("want 'something', got", extractPayload(m)) - } -} -func TestMatcher_extractPayloadSimpleType(t *testing.T) { - m := map[string]interface{}{ - "value": "string", - } - if !cmp.Equal(extractPayload(m), m) { - t.Fatal("extract payload diff: ", cmp.Diff(m, extractPayload(m))) - } -} - -func TestMatcher_extractPayloadEachLike(t *testing.T) { - m := map[string]interface{}{ - "access": EachLike(map[string]interface{}{ - "role": Term("admin", "admin|controller|user"), - }, 3), - } - want := map[string]interface{}{ - "access": []interface{}{ - map[string]interface{}{"role": "admin"}, - }, - } - - got := extractPayload(m) - if !cmp.Equal(want, got) { - t.Fatalf("want '%v', got '%v'. Diff: \n %v", want, got, cmp.Diff(want, got)) - } -} -func TestMatcher_extractPayloadComplex(t *testing.T) { - m := map[string]interface{}{ - "foo": Like("bar"), - "bar": Term("baz", "baz|bat"), - "baz": EachLike(map[string]interface{}{ - "bing": Like("bong"), - "boing": Like(1), - "bop": "bop", - }, 2), - } - want := map[string]interface{}{ - "foo": "bar", - "bar": "baz", - "baz": []interface{}{ - map[string]interface{}{ - "bing": "bong", - "boing": 1, - "bop": "bop", - }, - map[string]interface{}{ - "bing": "bong", - "boing": 1, - "bop": "bop", - }, - }, - } - - got := extractPayload(m) - if !cmp.Equal(want, got) { - t.Fatalf("want '%v', got '%v'. Diff: \n %v", want, got, cmp.Diff(want, got)) - } -} - func ExampleLike_string() { match := Like("myspecialvalue") fmt.Println(formatJSON(match)) diff --git a/dsl/message.go b/dsl/message.go index c153d5fbb..d4daf15c9 100644 --- a/dsl/message.go +++ b/dsl/message.go @@ -8,7 +8,7 @@ type Message struct { Content interface{} `json:"content,omitempty"` // Provider state to be written into the Pact file - State string `json:"providerState,omitempty"` + States []State `json:"providerStates,omitempty"` // Message metadata Metadata MapMatcher `json:"metadata,omitempty"` @@ -19,9 +19,16 @@ type Message struct { Args []string `json:"-"` } +// State specifies how the system should be configured when +// verified. e.g. "user A exists" +type State struct { + Name string `json:"name"` +} + // Given specifies a provider state. Optional. func (p *Message) Given(state string) *Message { - p.State = state + p.States = []State{State{state}} + return p } diff --git a/dsl/pact.go b/dsl/pact.go index 1fc9ed8b9..4445c49c9 100644 --- a/dsl/pact.go +++ b/dsl/pact.go @@ -315,14 +315,15 @@ var checkCliCompatibility = func() { } } -// VerifyMessageProducer accepts an instance of `*testing.T` +// 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) VerifyMessageProducer(t *testing.T, request types.VerifyRequest, handlers map[string]func(...interface{}) (map[string]interface{}, error)) (types.ProviderVerifierResponse, error) { +func (p *Pact) VerifyMessageProvider(t *testing.T, request types.VerifyRequest, handlers map[string]func(...interface{}) (map[string]interface{}, error)) (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 @@ -330,8 +331,11 @@ func (p *Pact) VerifyMessageProducer(t *testing.T, request types.VerifyRequest, // and error. The object will be marshalled to JSON for comparison. mux := http.NewServeMux() - // TODO: make this dynamic - port := 9393 + port, err := utils.GetFreePort() + if err != nil { + return response, fmt.Errorf("unable to allocate a port for verification: %v", err) + } + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -341,7 +345,6 @@ func (p *Pact) VerifyMessageProducer(t *testing.T, request types.VerifyRequest, r.Body.Close() if err != nil { - // TODO: How should we respond back to the verifier in this case? 50x? w.WriteHeader(http.StatusBadRequest) return } @@ -352,7 +355,6 @@ func (p *Pact) VerifyMessageProducer(t *testing.T, request types.VerifyRequest, f, messageFound := handlers[message.Description] if !messageFound { - // TODO: How should we respond back to the verifier in this case? 50x? w.WriteHeader(http.StatusNotFound) return } @@ -360,11 +362,7 @@ func (p *Pact) VerifyMessageProducer(t *testing.T, request types.VerifyRequest, // Execute function handler res, handlerErr := f() - fmt.Printf("[DEBUG] f() returned: %v", res) - if handlerErr != nil { - // TODO: How should we respond back to the verifier in this case? 50x? - fmt.Println("[ERROR] error handling function:", handlerErr) w.WriteHeader(http.StatusServiceUnavailable) return } @@ -376,7 +374,6 @@ func (p *Pact) VerifyMessageProducer(t *testing.T, request types.VerifyRequest, fmt.Println("[ERROR] error marshalling objcet:", errM) return } - fmt.Printf("[DEBUG] sending response body back to verifier %v", resBody) w.WriteHeader(http.StatusOK) w.Write(resBody) @@ -396,7 +393,7 @@ func (p *Pact) VerifyMessageProducer(t *testing.T, request types.VerifyRequest, if portErr != nil { t.Fatal("Error:", err) - return types.ProviderVerifierResponse{}, portErr + return response, portErr } res, err := p.VerifyProviderRaw(request) @@ -423,19 +420,24 @@ func (p *Pact) VerifyMessageConsumer(message *Message, handler func(Message) err log.Printf("[DEBUG] verify message") p.Setup(false) + // Reify the message back to its "example/generated" form + reified, err := p.pactClient.ReifyMessage(types.PactReificationRequest{ + Message: message.Content, + }) + if err != nil { + return fmt.Errorf("unable to convert consumer test to JSON: %v", err) + } + // Yield message, and send through handler function - // TODO: for now just call the handler - // TODO: unwrap the message back to its "generated" form generatedMessage := Message{ - Content: extractPayload(message.Content), + Content: reified, Description: message.Description, - State: message.State, + States: message.States, Metadata: message.Metadata, } - log.Println("[DEBUG] generated message from matcher:", generatedMessage.Content) - err := handler(generatedMessage) + err = handler(generatedMessage) if err != nil { return err } diff --git a/examples/messages/consumer/message_pact_consumer_test.go b/examples/messages/consumer/message_pact_consumer_test.go index 976864eed..ae91b9433 100644 --- a/examples/messages/consumer/message_pact_consumer_test.go +++ b/examples/messages/consumer/message_pact_consumer_test.go @@ -37,7 +37,7 @@ func TestMessageConsumer_Success(t *testing.T) { }) err := pact.VerifyMessageConsumer(message, func(m dsl.Message) error { - t.Logf("[DEBUG] calling message handler func with arguments: %v \n", m.Content) + t.Logf("[DEBUG] calling message handler func with arguments: %+v \n", m.Content) body := m.Content.(map[string]interface{}) @@ -85,11 +85,10 @@ var logDir = fmt.Sprintf("%s/log", dir) func createPact() dsl.Pact { // Create Pact connecting to local Daemon return dsl.Pact{ - Consumer: "billy", - Provider: "bobby", - LogDir: logDir, - // PactDir: pactDir, // TODO: this seems to cause an issue "NoMethodError: undefined method `content' for #" - PactDir: "/tmp", + Consumer: "billy", + Provider: "bobby", + LogDir: logDir, + PactDir: pactDir, // TODO: this seems to cause an issue "NoMethodError: undefined method `content' for #" LogLevel: "DEBUG", PactFileWriteMode: "update", } diff --git a/examples/messages/provider/message_pact_provider_test.go b/examples/messages/provider/message_pact_provider_test.go index 56da96386..d0ae7ed74 100644 --- a/examples/messages/provider/message_pact_provider_test.go +++ b/examples/messages/provider/message_pact_provider_test.go @@ -36,7 +36,7 @@ func TestMessageProvider_Success(t *testing.T) { // TODO: Add function mappings to the VerifyRequest type (or have separate one for producer) // this can't happen until we remove the RPC shit, because functions can't be mapped // over the wire - pact.VerifyMessageProducer(t, types.VerifyRequest{ + pact.VerifyMessageProvider(t, types.VerifyRequest{ ProviderBaseURL: fmt.Sprintf("http://localhost:%d", port), PactURLs: []string{filepath.ToSlash(fmt.Sprintf("%s/message-pact.json", pactDir))}, }, functionMappings) diff --git a/types/pact_message_request.go b/types/pact_message_request.go index 0f1fe8139..09ea4dc4d 100644 --- a/types/pact_message_request.go +++ b/types/pact_message_request.go @@ -50,6 +50,8 @@ func (m *PactMessageRequest) Validate() error { m.Provider, "--pact-dir", m.PactDir, + "--pact-specification-version", + "3", }...) return nil diff --git a/types/pact_reification_request.go b/types/pact_reification_request.go new file mode 100644 index 000000000..15fc17577 --- /dev/null +++ b/types/pact_reification_request.go @@ -0,0 +1,32 @@ +package types + +import "encoding/json" + +// PactReificationRequest contains the response from the Pact Message +// CLI execution. +type PactReificationRequest struct { + + // Message is the object to be marshalled to JSON + Message interface{} + + // Args are the arguments sent to to the message service + Args []string +} + +// Validate checks all things are well and constructs +// the CLI args to the message service +func (m *PactReificationRequest) Validate() error { + m.Args = []string{} + + body, err := json.Marshal(m.Message) + if err != nil { + return err + } + + m.Args = append(m.Args, []string{ + "reify", + string(body), + }...) + + return nil +}