diff --git a/transaction/transaction.go b/transaction/transaction.go index 4139826..6161cb5 100644 --- a/transaction/transaction.go +++ b/transaction/transaction.go @@ -1,36 +1,126 @@ package transaction -import "encoding/json" +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" +) // Transaction represents a Dredd transaction object. // http://dredd.readthedocs.org/en/latest/hooks/#transaction-object-structure type Transaction struct { - Name string `json:"name,omitempty"` - Host string `json:"host,omitempty"` - Port string `json:"port,omitempty"` - Protocol string `json:"protocol,omitempty"` - FullPath string `json:"fullPath,omitempty"` - Request *struct { - Body string `json:"body,omitempty"` - Headers map[string]interface{} `json:"headers,omitempty"` - URI string `json:"uri,omitempty"` - Method string `json:"method,omitempty"` - } `json:"request,omitempty"` + Name string `json:"name,omitempty"` + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Protocol string `json:"protocol,omitempty"` + FullPath string `json:"fullPath,omitempty"` + Request *Request `json:"request,omitempty"` Expected *json.RawMessage `json:"expected,omitempty"` - Real *struct { - Body string `json:"body"` - Headers map[string]interface{} `json:"headers"` - StatusCode int `json:"statusCode"` - } `json:"real,omitempty"` - Origin *json.RawMessage `json:"origin,omitempty"` - Test *json.RawMessage `json:"test,omitempty"` - Results *json.RawMessage `json:"results,omitempty"` - Skip bool `json:"skip,omitempty"` - Fail interface{} `json:"fail,omitempty"` + Real *RealRequest `json:"real,omitempty"` + Origin *json.RawMessage `json:"origin,omitempty"` + Test *json.RawMessage `json:"test,omitempty"` + Results *json.RawMessage `json:"results,omitempty"` + Skip bool `json:"skip,omitempty"` + Fail interface{} `json:"fail,omitempty"` TestOrder []string `json:"hooks_modifications,omitempty"` } +type Request struct { + Body string `json:"body,omitempty"` + Headers Headers `json:"headers,omitempty"` + URI string `json:"uri,omitempty"` + Method string `json:"method,omitempty"` +} + +type RealRequest struct { + Body string `json:"body"` + Headers Headers `json:"headers"` + StatusCode int `json:"statusCode"` +} + +type Headers map[string][]string + +func (hdrs *Headers) MarshalJSON() ([]byte, error) { + buf := bytes.Buffer{} + buf.WriteByte('{') + + first := true + for k, v := range *hdrs { + if first { + first = false + } else { + buf.WriteString(",") + } + numValues := len(v) + writeJSONKeyToBuffer(&buf, string(k)) + if numValues == 1 { + writeJSONValueToBuffer(&buf, v[0]) + continue + } + data, err := json.Marshal(v) + + if err != nil { + return nil, err + } + buf.WriteString(string(data)) + } + buf.WriteByte('}') + return buf.Bytes(), nil +} + +func writeJSONKeyToBuffer(buf *bytes.Buffer, key string) { + writeJSONValueToBuffer(buf, key) + buf.WriteByte(':') +} + +func writeJSONValueToBuffer(buf *bytes.Buffer, key string) { + buf.WriteString(`"` + key + `"`) +} + +func (hdrs *Headers) UnmarshalJSON(data []byte) error { + var headers map[string]interface{} = make(map[string]interface{}) + err := json.Unmarshal(data, &headers) + + if err != nil { + return err + } + + *hdrs = make(map[string][]string) + for k := range headers { + header := headers[k] + value := reflect.ValueOf(header) + kind := value.Kind() + + switch kind { + case reflect.Float32, reflect.Float64, reflect.Int, reflect.String: + (*hdrs)[k] = []string{ + stringFromValue(value), + } + case reflect.Slice: + for i := 0; i < value.Len(); i++ { + (*hdrs)[k] = append((*hdrs)[k], stringFromValueAtIndex(value, i)) + } + + default: + // TODO: Figure out how to use this + panic(fmt.Sprintf("unsupported Kind %s", kind.String())) + } + + } + + return nil +} + +func stringFromValueAtIndex(val reflect.Value, index int) string { + return fmt.Sprint(val.Index(index).Interface()) +} + +func stringFromValue(val reflect.Value) string { + return fmt.Sprint(val.Interface()) +} + // AddTestOrderPoint adds a value to the hooks_modification key used when // running dredd with TEST_DREDD_HOOKS_HANDLER_ORDER enabled. func (t *Transaction) AddTestOrderPoint(value string) { diff --git a/transaction/transaction_test.go b/transaction/transaction_test.go index 465b3a1..e4dcde0 100644 --- a/transaction/transaction_test.go +++ b/transaction/transaction_test.go @@ -1,6 +1,14 @@ package transaction -import "testing" +import ( + "encoding/json" + "reflect" + "testing" +) + +type HeadersWrapper struct { + Headers Headers `json:"headers,omitempty"` +} func TestSettingFailToString(t *testing.T) { trans := Transaction{} @@ -10,3 +18,66 @@ func TestSettingFailToString(t *testing.T) { t.Errorf("Transaction.Fail member must be able to be set as string") } } + +func TestHeadersJsonUnmarshal(t *testing.T) { + data := []byte(`{ + "headers":{ + "User-Agent":"Dredd/1.4.0 (Darwin 15.4.0; x64)", + "Content-Length":11, + "Set-Cookie":["Test=Yo"] + } + }`) + + hdrs := &HeadersWrapper{} + err := json.Unmarshal(data, hdrs) + if err != nil { + t.Errorf("Error %v", err) + } + + if !reflect.DeepEqual(hdrs.Headers["Set-Cookie"], []string{"Test=Yo"}) { + t.Errorf("Set-Cookie should be slice with length of 1") + } + + if !reflect.DeepEqual(hdrs.Headers["User-Agent"], []string{"Dredd/1.4.0 (Darwin 15.4.0; x64)"}) { + t.Errorf("User-Agent should be converted to a slice") + } + + if !reflect.DeepEqual(hdrs.Headers["Content-Length"], []string{"11"}) { + t.Errorf("Content-Length should be converted to a string and be a slice") + } +} + +func TestHeadersMarshalJSON(t *testing.T) { + headers := make(map[string][]string) + headers["Content-Type"] = []string{"9"} + headers["User-Agent"] = []string{"Dredd/1.4.0 (Darwin 15.4.0; x64)"} + headers["Set-Cookie"] = []string{"Test=Yo", "Another=This"} + wrapper := &HeadersWrapper{ + Headers: headers, + } + + data, err := json.Marshal(wrapper) + + if err != nil { + t.Errorf("json.Marshal failed with %v", err) + } + + var expected interface{} + err = json.Unmarshal([]byte(`{"headers":{"Content-Type":"9","User-Agent":"Dredd/1.4.0 (Darwin 15.4.0; x64)","Set-Cookie":["Test=Yo","Another=This"]}}`), &expected) + + if err != nil { + t.Errorf("failed to unmarshal expected json string") + } + + var actual interface{} + err = json.Unmarshal(data, &actual) + + if err != nil { + t.Errorf("failed to unmarshal Marshaled json") + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("json.Marshal failed, data \n %#v did not matched expected \n %#v", actual, expected) + + } +}