pkg/cloudevents/handler_test.go

526 lines
16 KiB
Go

/*
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 (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"knative.dev/pkg/cloudevents"
)
func TestHandlerTypeErrors(t *testing.T) {
for _, test := range []struct {
name string
param interface{}
err string
}{
{
name: "non-func",
param: 5,
err: "must pass a function to handle events",
},
{
name: "wrong param count",
param: func(context.Context, interface{}, interface{}) {},
err: "Expected a function taking either no parameters, a context.Context, or (context.Context, any); function has too many parameters (3)",
},
{
name: "wrong first parameter type",
param: func(int) {},
err: "Expected a function taking either no parameters, a context.Context, or (context.Context, any); cannot convert parameter 0 from int to context.Context",
},
{
name: "wrong 3-arg return type",
param: func() (interface{}, interface{}, error) { return nil, nil, nil },
err: "Expected a function returning either nothing, an error, (any, error), or (any, SendContext, error); cannot convert return type 1 from interface {} to SendContext",
},
{
name: "wrong return count",
param: func() (interface{}, cloudevents.EventContext, error, interface{}) {
return nil, cloudevents.EventContext{}, nil, nil
},
err: "Expected a function returning either nothing, an error, (any, error), or (any, SendContext, error); function has too many return types (4)",
},
{
name: "invalid return type",
param: func() interface{} { return nil },
err: "Expected a function returning either nothing, an error, (any, error), or (any, SendContext, error); cannot convert return type 0 from interface {} to error",
},
} {
t.Run(test.name, func(t *testing.T) {
h := cloudevents.Handler(test.param)
err, ok := h.(error)
if !ok {
t.Fatalf("Expected Handler() to fail with %v, passed", test.err)
}
if !strings.Contains(err.Error(), test.err) {
t.Errorf("Expected Handler() to fail. want %q, got %v", test.err, err)
}
// Attempt to call the returned Handler to verify it fails.
srv := httptest.NewServer(h)
defer srv.Close()
type E struct{}
req, err := cloudevents.NewRequest(srv.URL, E{}, cloudevents.EventContext{
EventID: "1", EventType: "a", Source: "one"})
if err != nil {
t.Errorf("Couldn't construct request: %v", err)
}
if resp, err := srv.Client().Do(req); err != nil {
t.Errorf("Failed to Post event: %v", resp)
} else if resp.StatusCode != http.StatusNotImplemented {
t.Errorf("Expected error status. got %d, got %d", resp.StatusCode, http.StatusNotImplemented)
}
})
}
}
func TestHandlerValidTypes(t *testing.T) {
for _, test := range []struct {
name string
f interface{}
}{
{
name: "no in, no out",
f: func() {},
}, {
name: "one in, no out",
f: func(context.Context) {},
}, {
name: "interface in, no out",
f: func(context.Context, io.Reader) {},
}, {
name: "value-type in, no out",
f: func(context.Context, int) {},
}, {
name: "pointer-type in, no out",
f: func(context.Context, *int) {},
}, {
name: "no in, one out",
f: func() error { return nil },
}, {
name: "one in, one out",
f: func(context.Context) error { return nil },
}, {
name: "two in, one out",
f: func(context.Context, string) error { return nil },
}, {
name: "no in, two out",
f: func() (string, error) { return "", nil },
}, {
name: "one in, two out",
f: func(context.Context) (map[string]interface{}, error) { return nil, nil },
}, {
name: "two in, two out",
f: func(context.Context, io.Reader) (interface{}, error) { return nil, nil },
},
} {
t.Run(test.name, func(t *testing.T) {
if err, ok := cloudevents.Handler(test.f).(error); ok {
t.Errorf("%q failed: %v", test.name, err)
}
})
}
}
func TestParameterMarsahlling(t *testing.T) {
type Data struct {
Message string
}
expectedData := Data{Message: "Hello, world!"}
expectedContext := &cloudevents.EventContext{
CloudEventsVersion: cloudevents.CloudEventsVersion,
EventID: "1234",
Source: "tests:TestUntypedHandling",
EventType: "dev.eventing.test",
EventTime: time.Now().UTC(),
ContentType: "application/json",
Extensions: map[string]interface{}{},
}
var wasCalled = false
for _, marshaller := range []struct {
name string
val cloudevents.HTTPMarshaller
}{
{
name: "structured",
val: cloudevents.Structured,
}, {
name: "binary",
val: cloudevents.Binary,
},
} {
for _, test := range []struct {
name string
generator func(t *testing.T) http.Handler
}{
{
name: "no parameters",
generator: func(t *testing.T) http.Handler {
return cloudevents.Handler(func() {
wasCalled = true
})
},
},
{
name: "one parameter",
generator: func(t *testing.T) http.Handler {
return cloudevents.Handler(func(ctx context.Context) {
eventContext := cloudevents.FromContext(ctx)
if !reflect.DeepEqual(expectedContext, eventContext) {
t.Fatalf("Did not get expected context; wanted=%s; got=%s", spew.Sdump(expectedContext), spew.Sdump(eventContext))
}
wasCalled = true
})
},
}, {
name: "two parameters (struct type)",
generator: func(t *testing.T) http.Handler {
return cloudevents.Handler(func(ctx context.Context, data Data) {
eventContext := cloudevents.FromContext(ctx)
if !reflect.DeepEqual(expectedContext, eventContext) {
t.Fatalf("Did not get expected context; wanted=%s; got=%s", spew.Sdump(expectedContext), spew.Sdump(eventContext))
}
if !reflect.DeepEqual(expectedData, data) {
t.Fatalf("Did not get expected data; wanted=%s; got=%s", spew.Sdump(expectedData), spew.Sdump(data))
}
wasCalled = true
})
},
}, {
name: "two parameters (pointer type)",
generator: func(t *testing.T) http.Handler {
return cloudevents.Handler(func(ctx context.Context, data *Data) {
eventContext := cloudevents.FromContext(ctx)
if !reflect.DeepEqual(expectedContext, eventContext) {
t.Fatalf("Did not get expected context; wanted=%s; got=%s", spew.Sdump(expectedContext), spew.Sdump(eventContext))
}
if !reflect.DeepEqual(expectedData, *data) {
t.Fatalf("Did not get expected data; wanted=%s; got=%s", spew.Sdump(&expectedData), spew.Sdump(data))
}
wasCalled = true
})
},
}, {
name: "two parameters (untyped)",
generator: func(t *testing.T) http.Handler {
return cloudevents.Handler(func(ctx context.Context, data map[string]interface{}) {
eventContext := cloudevents.FromContext(ctx)
if !reflect.DeepEqual(expectedContext, eventContext) {
t.Fatalf("Did not get expected context; wanted=%s; got=%s", spew.Sdump(expectedContext), spew.Sdump(eventContext))
}
b, err := json.Marshal(expectedData)
if err != nil {
t.Fatal("Failed to serialize expected data", err)
}
var expectedUntyped map[string]interface{}
err = json.Unmarshal(b, &expectedUntyped)
if err != nil {
t.Fatal("Failed to deserialize expected data", err)
}
if !reflect.DeepEqual(expectedUntyped, data) {
t.Fatalf("Did not get expected data; wanted=%s; got=%s", spew.Sdump(expectedUntyped), spew.Sdump(data))
}
wasCalled = true
})
},
},
} {
t.Run(fmt.Sprintf("%s: %s", marshaller.name, test.name), func(t *testing.T) {
wasCalled = false
handler := test.generator(t)
if err, ok := handler.(error); ok {
t.Errorf("Handler() failed: %v", err)
return
}
srv := httptest.NewServer(handler)
defer srv.Close()
req, err := marshaller.val.NewRequest(srv.URL, expectedData, *expectedContext)
if err != nil {
t.Fatal("Failed to marshal request ", err)
}
res, err := srv.Client().Do(req)
if err != nil {
t.Fatal("Failed to send request")
}
if res.StatusCode/100 != 2 {
t.Fatal("Got non-successful response: ", res.StatusCode)
}
if !wasCalled {
t.Fatal("Handler was never called")
}
})
}
}
}
func TestReturnTypeRendering(t *testing.T) {
eventData := map[string]interface{}{
"unused": "data",
}
type RetVal struct {
ID interface{}
}
eventContext := cloudevents.V01EventContext{
CloudEventsVersion: cloudevents.V01CloudEventsVersion,
EventID: "1234",
Source: "tests:TestUntypedHandling",
EventType: "dev.eventing.test",
Extensions: map[string]interface{}{"foo": "bar"},
}
for _, test := range []struct {
name string
expectedStatus int
expectedResponse string
expectedHeader http.Header
handler http.Handler
}{
{
name: "no return",
expectedStatus: http.StatusNoContent,
handler: cloudevents.Handler(func() {}),
}, {
name: "nil error return",
expectedStatus: http.StatusNoContent,
handler: cloudevents.Handler(func() error {
return nil
}),
}, {
name: "non-nil error return (one return type)",
expectedStatus: http.StatusInternalServerError,
handler: cloudevents.Handler(func() error {
return errors.New("Some error")
}),
expectedResponse: "Internal server error",
}, {
name: "successful return",
expectedStatus: http.StatusOK,
handler: cloudevents.Handler(func() (map[string]interface{}, error) {
return map[string]interface{}{"hello": "world"}, nil
}),
expectedResponse: `{"hello":"world"}`,
expectedHeader: http.Header{
// Default values filled in
"Ce-Eventtype": {"dev.knative.pkg.cloudevents.unknown"},
"Ce-Source": {"unknown"},
},
}, {
name: "response headers",
expectedStatus: http.StatusOK,
handler: cloudevents.Handler(func() (map[string]interface{}, cloudevents.V01EventContext, error) {
ec := cloudevents.EventContext{
EventID: "1234",
EventType: "dev.knative.test.thing",
Source: "this-is-it",
SchemaURL: "http://example.com/schema",
Extensions: map[string]interface{}{"foo": "bar"},
}
return map[string]interface{}{"hello": "world"}, ec, nil
}),
expectedResponse: `{"hello":"world"}`,
expectedHeader: http.Header{
"Ce-Eventid": {"1234"},
"Ce-Eventtype": {"dev.knative.test.thing"},
"Ce-Source": {"this-is-it"},
"Ce-Schemaurl": {"http://example.com/schema"},
"Ce-X-Foo": {`"bar"`},
},
}, {
name: "v02 response headers",
expectedStatus: http.StatusOK,
handler: cloudevents.Handler(func() (map[string]interface{}, cloudevents.V02EventContext, error) {
ec := cloudevents.V02EventContext{
ID: "1234",
Type: "dev.knative.test.thing",
Source: "this-is-it",
SchemaURL: "http://example.com/schema",
Extensions: map[string]interface{}{"foo": "bar"},
}
return map[string]interface{}{"hello": "world"}, ec, nil
}),
expectedResponse: `{"hello":"world"}`,
expectedHeader: http.Header{
"Ce-Id": {"1234"},
"Ce-Type": {"dev.knative.test.thing"},
"Ce-Source": {"this-is-it"},
"Ce-Schemaurl": {"http://example.com/schema"},
"Ce-Foo": {`"bar"`},
},
}, {
name: "non-nil error return (two return types)",
expectedStatus: http.StatusInternalServerError,
handler: cloudevents.Handler(func() (map[string]interface{}, error) {
return map[string]interface{}{"hello": "world"}, errors.New("Errors take precedence")
}),
expectedResponse: "Internal server error",
},
{
name: "non-nil content return",
expectedStatus: http.StatusOK,
handler: cloudevents.Handler(func() (map[string]interface{}, error) {
return map[string]interface{}{"hello": "world"}, nil
}),
expectedResponse: `{"hello":"world"}`,
},
{
name: "bad JSON content",
expectedStatus: http.StatusInternalServerError,
handler: cloudevents.Handler(func() (RetVal, error) {
return RetVal{ID: func() {}}, nil
}),
},
} {
t.Run(test.name, func(t *testing.T) {
if err, ok := test.handler.(error); ok {
t.Errorf("Handler() failed: %v", err)
return
}
srv := httptest.NewServer(test.handler)
defer srv.Close()
req, err := cloudevents.NewRequest(srv.URL, eventData, eventContext)
if err != nil {
t.Fatal("Failed to marshal request ", err)
}
res, err := srv.Client().Do(req)
if err != nil {
t.Fatal("Failed to send request")
}
defer res.Body.Close()
if test.expectedStatus != res.StatusCode {
t.Fatalf("Wrong status code from event handler; wanted=%d; got=%d", test.expectedStatus, res.StatusCode)
}
if test.expectedHeader != nil {
for k := range test.expectedHeader {
if res.Header.Get(k) != test.expectedHeader.Get(k) {
t.Fatalf("Wrong response header %q: wanted=%q; got=%q", k, test.expectedHeader.Get(k), res.Header.Get(k))
}
}
}
if test.expectedResponse != "" {
b, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatal("Failed to read response body:", err)
}
resBody := string(b)
if test.expectedResponse != resBody {
t.Fatalf("Got unexpected respnose string; wanted=%q; got=%q", test.expectedResponse, resBody)
}
}
})
}
}
func TestMux(t *testing.T) {
type TypeA struct {
Greeting string
}
type TypeB struct {
Farewell string
}
eventA := TypeA{
Greeting: "Hello, world!",
}
eventB := TypeB{
Farewell: "Hasta la vista",
}
contextA := &cloudevents.V01EventContext{
CloudEventsVersion: cloudevents.V01CloudEventsVersion,
EventID: "1234",
EventType: "org.A.test",
Source: "test:TestMux",
ContentType: "application/json",
Extensions: map[string]interface{}{},
}
contextB := &cloudevents.V02EventContext{
SpecVersion: cloudevents.V02CloudEventsVersion,
ID: "5678",
Type: "org.B.test",
Source: "test:TestMux",
ContentType: "application/json",
Extensions: map[string]interface{}{},
}
sawA, sawB := false, false
mux := cloudevents.NewMux()
err := mux.Handle("org.A.test", func(ctx context.Context, data TypeA) error {
sawA = true
context := cloudevents.FromContext(ctx)
if !reflect.DeepEqual(eventA, data) {
t.Fatalf("Got wrong data for event A; wanted=%s; got=%s", eventA, data)
}
if !reflect.DeepEqual(contextA, context) {
t.Fatalf("Got wrong context for event A; wanted=%s; got=%s", contextA, context)
}
return nil
})
if err != nil {
t.Fatalf("mux.Handle('org.A.test') failed: %v", err)
}
err = mux.Handle("org.B.test", func(ctx context.Context, data TypeB) error {
sawB = true
context := cloudevents.FromContext(ctx)
if !reflect.DeepEqual(eventB, data) {
t.Fatalf("Got wrong data for event A; wanted=%s; got=%s", eventB, data)
}
if !reflect.DeepEqual(contextB, context) {
t.Fatalf("Got wrong context for event A; wanted=%s; got=%s", contextB, context)
}
return nil
})
if err != nil {
t.Fatalf("mux.Handle('org.B.test') failed: %v", err)
}
srv := httptest.NewServer(mux)
defer srv.Close()
req, err := cloudevents.NewRequest(srv.URL, eventA, *contextA)
if err != nil {
t.Fatal("Failed to marshal request for eventA", err)
}
if _, err := srv.Client().Do(req); err != nil {
t.Fatal("Failed to send eventA", err)
}
req, err = cloudevents.NewRequest(srv.URL, eventB, *contextB)
if err != nil {
t.Fatal("Failed to marshal request for eventB", err)
}
if _, err := srv.Client().Do(req); err != nil {
t.Fatal("Failed to send eventB", err)
}
if !sawA {
t.Fatal("Handler for eventA never called")
}
if !sawB {
t.Fatal("Handler for eventB never called")
}
}