diff --git a/pkg/rest/apiv1_controller_test.go b/pkg/rest/apiv1_controller_test.go index b31cfcb..4906d21 100644 --- a/pkg/rest/apiv1_controller_test.go +++ b/pkg/rest/apiv1_controller_test.go @@ -6,7 +6,6 @@ import ( "net/mail" "net/textproto" "os" - "strings" "testing" "time" @@ -68,22 +67,6 @@ func TestRestMailboxList(t *testing.T) { } // Test JSON message headers - data1 := &InputMessageData{ - Mailbox: "good", - ID: "0001", - From: "", - To: []string{""}, - Subject: "subject 1", - Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)), - } - data2 := &InputMessageData{ - Mailbox: "good", - ID: "0002", - From: "", - To: []string{""}, - Subject: "subject 2", - Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)), - } meta1 := message.Metadata{ Mailbox: "good", ID: "0001", @@ -114,25 +97,6 @@ func TestRestMailboxList(t *testing.T) { } // Check JSON - got := w.Body.String() - testStrings := []string{ - `{"mailbox":"good","id":"0001","from":"\u003cfrom1@host\u003e",` + - `"to":["\u003cto1@host\u003e"],"subject":"subject 1",` + - `"date":"2012-02-01T10:11:12.000000253-00:13","size":0}`, - `{"mailbox":"good","id":"0002","from":"\u003cfrom2@host\u003e",` + - `"to":["\u003cto1@host\u003e"],"subject":"subject 2",` + - `"date":"2012-07-01T10:11:12.000000253-00:11","size":0}`, - } - for _, ts := range testStrings { - t.Run(ts, func(t *testing.T) { - if !strings.Contains(got, ts) { - t.Errorf("got:\n%s\nwant to contain:\n%s", got, ts) - } - }) - } - - // Check JSON - // TODO transitional while refactoring dec := json.NewDecoder(w.Body) var result []interface{} if err := dec.Decode(&result); err != nil { @@ -141,18 +105,21 @@ func TestRestMailboxList(t *testing.T) { if len(result) != 2 { t.Fatalf("Expected 2 results, got %v", len(result)) } - if errors := data1.CompareToJSONHeaderMap(result[0]); len(errors) > 0 { - t.Logf("%v", result[0]) - for _, e := range errors { - t.Error(e) - } - } - if errors := data2.CompareToJSONHeaderMap(result[1]); len(errors) > 0 { - t.Logf("%v", result[1]) - for _, e := range errors { - t.Error(e) - } - } + + decodedStringEquals(t, result, "[0]/mailbox", "good") + decodedStringEquals(t, result, "[0]/id", "0001") + decodedStringEquals(t, result, "[0]/from", "") + decodedStringEquals(t, result, "[0]/to/[0]", "") + decodedStringEquals(t, result, "[0]/subject", "subject 1") + decodedStringEquals(t, result, "[0]/date", "2012-02-01T10:11:12.000000253-00:13") + decodedNumberEquals(t, result, "[0]/size", 0) + decodedStringEquals(t, result, "[1]/mailbox", "good") + decodedStringEquals(t, result, "[1]/id", "0002") + decodedStringEquals(t, result, "[1]/from", "") + decodedStringEquals(t, result, "[1]/to/[0]", "") + decodedStringEquals(t, result, "[1]/subject", "subject 2") + decodedStringEquals(t, result, "[1]/date", "2012-07-01T10:11:12.000000253-00:11") + decodedNumberEquals(t, result, "[1]/size", 0) if t.Failed() { // Wait for handler to finish logging @@ -225,19 +192,6 @@ func TestRestMessage(t *testing.T) { }, }, } - data1 := &InputMessageData{ - Mailbox: "good", - ID: "0001", - From: "", - Subject: "subject 1", - Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)), - Header: mail.Header{ - "To": []string{"fred@fish.com", "keyword@nsa.gov"}, - "From": []string{"noreply@inbucket.org"}, - }, - Text: "This is some text", - HTML: "This is some HTML", - } mm.AddMessage("good", msg1) // Check return code @@ -251,19 +205,24 @@ func TestRestMessage(t *testing.T) { } // Check JSON - // TODO transitional while refactoring dec := json.NewDecoder(w.Body) var result map[string]interface{} if err := dec.Decode(&result); err != nil { t.Errorf("Failed to decode JSON: %v", err) } - if errors := data1.CompareToJSONMessageMap(result); len(errors) > 0 { - t.Logf("%v", result) - for _, e := range errors { - t.Error(e) - } - } + decodedStringEquals(t, result, "mailbox", "good") + decodedStringEquals(t, result, "id", "0001") + decodedStringEquals(t, result, "from", "") + decodedStringEquals(t, result, "to/[0]", "") + decodedStringEquals(t, result, "subject", "subject 1") + decodedStringEquals(t, result, "date", "2012-02-01T10:11:12.000000253-00:13") + decodedNumberEquals(t, result, "size", 0) + decodedStringEquals(t, result, "body/text", "This is some text") + decodedStringEquals(t, result, "body/html", "This is some HTML") + decodedStringEquals(t, result, "header/To/[0]", "fred@fish.com") + decodedStringEquals(t, result, "header/To/[1]", "keyword@nsa.gov") + decodedStringEquals(t, result, "header/From/[0]", "noreply@inbucket.org") if t.Failed() { // Wait for handler to finish logging diff --git a/pkg/rest/testutils_test.go b/pkg/rest/testutils_test.go index 23996a5..587c3b7 100644 --- a/pkg/rest/testutils_test.go +++ b/pkg/rest/testutils_test.go @@ -2,12 +2,12 @@ package rest import ( "bytes" - "fmt" "log" "net/http" "net/http/httptest" - "net/mail" - "time" + "strconv" + "strings" + "testing" "github.com/jhillyerd/inbucket/pkg/config" "github.com/jhillyerd/inbucket/pkg/message" @@ -15,146 +15,6 @@ import ( "github.com/jhillyerd/inbucket/pkg/server/web" ) -type InputMessageData struct { - Mailbox, ID, From, Subject string - To []string - Date time.Time - Size int - Header mail.Header - HTML, Text string -} - -// isJSONStringEqual is a utility function to return a nicely formatted message when -// comparing a string to a value received from a JSON map. -func isJSONStringEqual(key, expected string, received interface{}) (message string, ok bool) { - if value, ok := received.(string); ok { - if expected == value { - return "", true - } - return fmt.Sprintf("Expected value of key %v to be %q, got %q", key, expected, value), false - } - return fmt.Sprintf("Expected value of key %v to be a string, got %T", key, received), false -} - -// isJSONNumberEqual is a utility function to return a nicely formatted message when -// comparing an float64 to a value received from a JSON map. -func isJSONNumberEqual(key string, expected float64, received interface{}) (message string, ok bool) { - if value, ok := received.(float64); ok { - if expected == value { - return "", true - } - return fmt.Sprintf("Expected %v to be %v, got %v", key, expected, value), false - } - return fmt.Sprintf("Expected %v to be a string, got %T", key, received), false -} - -// CompareToJSONHeaderMap compares InputMessageData to a header map decoded from JSON, -// returning a list of things that did not match. -func (d *InputMessageData) CompareToJSONHeaderMap(json interface{}) (errors []string) { - if m, ok := json.(map[string]interface{}); ok { - if msg, ok := isJSONStringEqual(mailboxKey, d.Mailbox, m[mailboxKey]); !ok { - errors = append(errors, msg) - } - if msg, ok := isJSONStringEqual(idKey, d.ID, m[idKey]); !ok { - errors = append(errors, msg) - } - if msg, ok := isJSONStringEqual(fromKey, d.From, m[fromKey]); !ok { - errors = append(errors, msg) - } - for i, inputTo := range d.To { - if msg, ok := isJSONStringEqual(toKey, inputTo, m[toKey].([]interface{})[i]); !ok { - errors = append(errors, msg) - } - } - if msg, ok := isJSONStringEqual(subjectKey, d.Subject, m[subjectKey]); !ok { - errors = append(errors, msg) - } - exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00") - if msg, ok := isJSONStringEqual(dateKey, exDate, m[dateKey]); !ok { - errors = append(errors, msg) - } - if msg, ok := isJSONNumberEqual(sizeKey, float64(d.Size), m[sizeKey]); !ok { - errors = append(errors, msg) - } - return errors - } - panic(fmt.Sprintf("Expected map[string]interface{} in json, got %T", json)) -} - -// CompareToJSONMessageMap compares InputMessageData to a message map decoded from JSON, -// returning a list of things that did not match. -func (d *InputMessageData) CompareToJSONMessageMap(json interface{}) (errors []string) { - // We need to check the same values as header first - errors = d.CompareToJSONHeaderMap(json) - - if m, ok := json.(map[string]interface{}); ok { - // Get nested body map - if m[bodyKey] != nil { - if body, ok := m[bodyKey].(map[string]interface{}); ok { - if msg, ok := isJSONStringEqual(textKey, d.Text, body[textKey]); !ok { - errors = append(errors, msg) - } - if msg, ok := isJSONStringEqual(htmlKey, d.HTML, body[htmlKey]); !ok { - errors = append(errors, msg) - } - } else { - panic(fmt.Sprintf("Expected map[string]interface{} in json key %q, got %T", - bodyKey, m[bodyKey])) - } - } else { - errors = append(errors, fmt.Sprintf("Expected body in JSON %q but it was nil", bodyKey)) - } - exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00") - if msg, ok := isJSONStringEqual(dateKey, exDate, m[dateKey]); !ok { - errors = append(errors, msg) - } - if msg, ok := isJSONNumberEqual(sizeKey, float64(d.Size), m[sizeKey]); !ok { - errors = append(errors, msg) - } - - // Get nested header map - if m[headerKey] != nil { - if header, ok := m[headerKey].(map[string]interface{}); ok { - // Loop over input (expected) header names - for name, keyInputHeaders := range d.Header { - // Make sure expected header name exists in received JSON - if keyOutputVals, ok := header[name]; ok { - if keyOutputHeaders, ok := keyOutputVals.([]interface{}); ok { - // Loop over input (expected) header values - for _, inputHeader := range keyInputHeaders { - hasValue := false - // Look for expected value in received headers - for _, outputHeader := range keyOutputHeaders { - if inputHeader == outputHeader { - hasValue = true - break - } - } - if !hasValue { - errors = append(errors, fmt.Sprintf( - "JSON %v[%q] missing value %q", headerKey, name, inputHeader)) - } - } - } else { - // keyOutputValues was not a slice of interface{} - panic(fmt.Sprintf("Expected []interface{} in %v[%q], got %T", headerKey, - name, keyOutputVals)) - } - } else { - errors = append(errors, fmt.Sprintf("JSON %v missing key %q", headerKey, name)) - } - } - } - } else { - errors = append(errors, fmt.Sprintf("Expected header in JSON %q but it was nil", headerKey)) - } - } else { - panic(fmt.Sprintf("Expected map[string]interface{} in json, got %T", json)) - } - - return errors -} - func testRestGet(url string) (*httptest.ResponseRecorder, error) { req, err := http.NewRequest("GET", url, nil) req.Header.Add("Accept", "application/json") @@ -184,3 +44,89 @@ func setupWebServer(mm message.Manager) *bytes.Buffer { return buf } + +func decodedNumberEquals(t *testing.T, json interface{}, path string, want float64) { + t.Helper() + els := strings.Split(path, "/") + val, msg := getDecodedPath(json, els...) + if msg != "" { + t.Errorf("JSON result%s", msg) + return + } + if got, ok := val.(float64); ok { + if got == want { + return + } + } + t.Errorf("JSON result/%s == %v (%T), want: %v", path, val, val, want) +} + +func decodedStringEquals(t *testing.T, json interface{}, path string, want string) { + t.Helper() + els := strings.Split(path, "/") + val, msg := getDecodedPath(json, els...) + if msg != "" { + t.Errorf("JSON result%s", msg) + return + } + if got, ok := val.(string); ok { + if got == want { + return + } + } + t.Errorf("JSON result/%s == %v (%T), want: %v", path, val, val, want) +} + +// getDecodedPath recursively navigates the specified path, returing the requested element. If +// something goes wrong, the returned string will contain an explanation. +// +// Named path elements require the parent element to be a map[string]interface{}, numbers in square +// brackets require the parent element to be a []interface{}. +// +// getDecodedPath(o, "users", "[1]", "name") +// +// is equivalent to the JavaScript: +// +// o.users[1].name +// +func getDecodedPath(o interface{}, path ...string) (interface{}, string) { + if len(path) == 0 { + return o, "" + } + if o == nil { + return nil, " is nil" + } + key := path[0] + present := false + var val interface{} + if key[0] == '[' { + // Expecting slice. + index, err := strconv.Atoi(strings.Trim(key, "[]")) + if err != nil { + return nil, "/" + key + " is not a slice index" + } + oslice, ok := o.([]interface{}) + if !ok { + return nil, " is not a slice" + } + if index >= len(oslice) { + return nil, "/" + key + " is out of bounds" + } + val, present = oslice[index], true + } else { + // Expecting map. + omap, ok := o.(map[string]interface{}) + if !ok { + return nil, " is not a map" + } + val, present = omap[key] + } + if !present { + return nil, "/" + key + " is missing" + } + result, msg := getDecodedPath(val, path[1:]...) + if msg != "" { + return nil, "/" + key + msg + } + return result, "" +}