/* Copyright 2018 The Knative Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cloudevents_test import ( "encoding/json" "encoding/xml" "errors" "fmt" "io/ioutil" "net/http" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "knative.dev/pkg/cloudevents" ) type FirestoreDocument struct { Name string `json:"name"` Fields map[string]interface{} `json:"fields"` CreateTime time.Time `json:"createTime"` UpdateTime time.Time `json:"updateTime"` } var ( webhook = "http://localhost/:sendEvent" ) // Wrap the global functions in an HTTPMarshaller interface for table-driven testing: type defaultMarshaller int var Default defaultMarshaller = 0 func (defaultMarshaller) FromRequest(data interface{}, r *http.Request) (cloudevents.LoadContext, error) { return cloudevents.FromRequest(data, r) } func (defaultMarshaller) NewRequest(urlString string, data interface{}, context cloudevents.SendContext) (*http.Request, error) { return cloudevents.NewRequest(urlString, data, context) } type desiredVersion func(cloudevents.LoadContext) cloudevents.ContextType var ( v01 = func(a cloudevents.LoadContext) cloudevents.ContextType { tmp := a.AsV01(); return &tmp } v02 = func(a cloudevents.LoadContext) cloudevents.ContextType { tmp := a.AsV02(); return &tmp } ) func TestValidRoundTrips(t *testing.T) { doc := FirestoreDocument{ Name: "projects/demo/databases/default/documents/users/inlined", Fields: map[string]interface{}{ "project": "eventing", "handle": "@inlined", }, CreateTime: time.Date(1985, 6, 5, 12, 0, 0, 0, time.UTC), UpdateTime: time.Now().UTC(), } service := "firestore.googleapis.com" encoderSets := map[string][]desiredVersion{ "v1-v1": {v01, v01}, "v1-v2": {v01, v02}, "v2-v1": {v02, v01}, "v2-v2": {v02, v02}, } context := &cloudevents.EventContext{ CloudEventsVersion: "0.1", EventID: "eventid-123", EventTime: doc.UpdateTime, EventType: "google.firestore.document.create", EventTypeVersion: "v1beta2", SchemaURL: "http://type.googleapis.com/google.firestore.v1beta1.Document", ContentType: "application/json", Source: fmt.Sprintf("//%s/%s", service, doc.Name), Extensions: map[string]interface{}{ "purpose": "tbd", "super": map[string]interface{}{"cali": "fragilistic", "expi": "alidocious"}, }, } for _, test := range []struct { name string encoder cloudevents.HTTPMarshaller decoder cloudevents.HTTPMarshaller }{ { name: "binary -> binary", encoder: cloudevents.Binary, decoder: cloudevents.Binary, }, { name: "binary -> default", encoder: cloudevents.Binary, decoder: Default, }, { name: "structured -> structured", encoder: cloudevents.Structured, decoder: cloudevents.Structured, }, { name: "structured -> default", encoder: cloudevents.Structured, decoder: Default, }, } { for encoding, convert := range encoderSets { testName := test.name + "-" + encoding t.Run(testName, func(t *testing.T) { req, err := test.encoder.NewRequest(webhook, doc, convert[0](context)) if err != nil { t.Fatalf("Failed to encode event %s", err) } var foundData FirestoreDocument foundContext, err := test.decoder.FromRequest(&foundData, req) if err != nil { t.Fatalf("Failed to decode event %s", err) } foundContext = convert[1](foundContext) if diff := cmp.Diff(convert[0](context), convert[0](foundContext)); diff != "" { t.Fatalf("%s: Context was transcoded lossily (-want +got): %s", testName, diff) } if diff := cmp.Diff(doc, foundData); diff != "" { t.Fatalf("%s: Data was transcoded lossily (-want +got): %s", testName, diff) } }) } } } type Address struct { City, State string } type Person struct { XMLName xml.Name `xml:"person"` Id int `xml:"id,attr"` FirstName string `xml:"name>first"` LastName string `xml:"name>last"` Age int `xml:"age"` Height float32 `xml:"height,omitempty"` Married bool Address Comment string `xml:",comment"` } func (Person) MarshalJSON() ([]byte, error) { return nil, errors.New("Person cannot be JSON encoded") } func (*Person) UnmarshalJSON([]byte) error { return errors.New("Person cannot be JSON decoded") } func TestXmlStructuredDecoding(t *testing.T) { person := &Person{ XMLName: xml.Name{ Local: "person", }, Id: 13, FirstName: "John", LastName: "Doe", Age: 42, Comment: " Need more details. ", Address: Address{"Hanga Roa", "Easter Island"}, } xmlPerson := ` John Doe 42 false Hanga Roa Easter Island ` xmlJsonSafe, err := json.Marshal(xmlPerson) if err != nil { t.Fatalf("Failed to create JSON encoded XML string: %s", err) } eventText := ` { "cloudEventsVersion": "0.1", "eventID": "1234", "eventType": "dev.eventing.test", "source": "tests://TextXmlStructuredEncoding", "contentType": "application/xml", "data": ` + string(xmlJsonSafe) + ` }` h := http.Header{} h.Set(cloudevents.HeaderContentType, cloudevents.ContentTypeStructuredJSON) req := &http.Request{ Header: h, Body: ioutil.NopCloser(strings.NewReader(eventText)), } var foundPerson Person _, err = cloudevents.FromRequest(&foundPerson, req) if err != nil { t.Fatalf("Failed to parse cross-encoded request: %s", err) } if diff := cmp.Diff(person, &foundPerson); diff != "" { t.Fatalf("Failed to parse xml-encoded data (-want +got): %s", diff) } } func TestExtensionsAreNeverNil(t *testing.T) { r := &http.Request{ Header: http.Header{ cloudevents.HeaderContentType: []string{cloudevents.ContentTypeStructuredJSON}, }, Body: ioutil.NopCloser(strings.NewReader(` { "cloudEventsVersion": "0.1", "eventID": "1234", "eventType": "dev.eventing.test", "source": "tests://TextXmlStructuredEncoding", "data": "hello, world" }`)), } var data interface{} ctx, err := cloudevents.FromRequest(&data, r) if err != nil { t.Fatal("Failed to parse request", err) } if ctx.AsV01().Extensions == nil { t.Fatal("v0.1 Extensions should never be nil") } if ctx.AsV02().Extensions == nil { t.Fatal("v0.2 Extensions should never be nil") } } func TestExtensionExtraction(t *testing.T) { h := http.Header{} h.Set(cloudevents.HeaderCloudEventsVersion, cloudevents.V01CloudEventsVersion) h.Set(cloudevents.HeaderContentType, cloudevents.ContentTypeBinaryJSON) h.Set(cloudevents.HeaderEventID, "1234") h.Set(cloudevents.HeaderEventType, "dev.eventing.test") h.Set(cloudevents.HeaderSource, "tests://TestExtensionExtraction") h.Set("CE-X-Prop1", "value1") h.Set("CE-X-Prop2", `{"nestedProp":"nestedValue"}`) b := strings.NewReader(`{"hello": "world"}`) r := &http.Request{ Header: h, Body: ioutil.NopCloser(b), } var data interface{} ctx, err := cloudevents.FromRequest(&data, r) if err != nil { t.Fatal("Failed to parse request", err) } expectedV01Extensions := map[string]interface{}{ "Prop1": "value1", "Prop2": map[string]interface{}{ "nestedProp": "nestedValue", }, } if diff := cmp.Diff(expectedV01Extensions, ctx.AsV01().Extensions); diff != "" { t.Fatalf("Did not parse expected extensions (-want + got): %s", diff) } }