mirror of https://github.com/knative/pkg.git
526 lines
16 KiB
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")
|
|
}
|
|
}
|