Skip to content

Commit

Permalink
bug: webhook return status
Browse files Browse the repository at this point in the history
  • Loading branch information
katallaxie authored Nov 27, 2024
1 parent 2abf631 commit b8f6295
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 4 deletions.
5 changes: 4 additions & 1 deletion cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ import (
"github.com/zeiss/typhoon/pkg/targets/reconciler/splunktarget"
)

const component = "typhoon-controller"

func main() {
ctx := signals.NewContext()

if namespace, set := os.LookupEnv("WORKING_NAMESPACE"); set {
ctx = injection.WithNamespaceScope(ctx, namespace)
}

sharedmain.MainWithContext(ctx, "typhoon-controller",
sharedmain.MainWithContext(ctx,
component,
cloudeventssource.NewController,
cloudeventstarget.NewController,
httppollersource.NewController,
Expand Down
4 changes: 3 additions & 1 deletion cmd/httppollersource-adapter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"github.com/zeiss/typhoon/pkg/sources/adapter/httppollersource"
)

const component = "httppollersource-adapter"

func main() {
adapter.Main("httppoller", httppollersource.NewEnvConfig, httppollersource.NewAdapter)
adapter.Main(component, httppollersource.NewEnvConfig, httppollersource.NewAdapter)
}
4 changes: 3 additions & 1 deletion pkg/sources/adapter/webhooksource/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (

// NewAdapter satisfies pkgadapter.AdapterConstructor.
func NewAdapter(ctx context.Context, envAcc adapter.EnvConfigAccessor, ceClient cloudevents.Client) adapter.Adapter {
logger := logging.FromContext(ctx)

mt := &adapter.MetricTag{
ResourceGroup: conv.String(sources.WebhookSourceResource),
Namespace: envAcc.GetNamespace(),
Expand All @@ -31,7 +33,7 @@ func NewAdapter(ctx context.Context, envAcc adapter.EnvConfigAccessor, ceClient
corsAllowOrigin: env.CORSAllowOrigin,

ceClient: ceClient,
logger: logging.FromContext(ctx),
logger: logger,
mt: mt,
}
}
4 changes: 3 additions & 1 deletion pkg/sources/adapter/webhooksource/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (h *webhookHandler) handleAll(ctx context.Context) http.HandlerFunc {
return
}

if utilx.And(utilx.NotEmpty(h.username), utilx.Empty(h.password)) {
if utilx.And(utilx.NotEmpty(h.username), utilx.NotEmpty(h.password)) {
us, ps, ok := r.BasicAuth()
if !ok {
h.handleError(errors.New("wrong authentication header"), http.StatusBadRequest, w)
Expand Down Expand Up @@ -159,6 +159,7 @@ func (h *webhookHandler) handleAll(ctx context.Context) http.HandlerFunc {
}
}
}

if h.extensionAttributesFrom.headers {
for k, v := range r.Header {
// Prevent Authorization header from being added
Expand All @@ -172,6 +173,7 @@ func (h *webhookHandler) handleAll(ctx context.Context) http.HandlerFunc {
}
continue
}

if k == "Ce-Subject" {
if len(v) != 0 {
event.SetSubject(v[0])
Expand Down
303 changes: 303 additions & 0 deletions pkg/sources/adapter/webhooksource/webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
package webhooksource

import (
"context"
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol"
"github.com/stretchr/testify/assert"

zapt "go.uber.org/zap/zaptest"

cloudevents "github.com/cloudevents/sdk-go/v2"
cloudeventst "github.com/cloudevents/sdk-go/v2/client/test"
)

const (
tEventType = "testType"
tEventSource = "testSource"
tHost = "test-host"
tResponseEventType = "testRespType"
tResponseEventSource = "testRespSource"
)

var (
expectedExtensionsBase = map[string]interface{}{
"host": tHost,
"method": http.MethodGet,
"path": "/",
}
)

func TestWebhookEvent(t *testing.T) {

logger := zapt.NewLogger(t).Sugar()

tc := map[string]struct {
body io.Reader

username string
password string
query string
headers map[string]string

expectedCode int
expectedResponseContains string
expectedEventData string
expectedExtensions map[string]interface{}
responseResult protocol.Result
responseEvent *event.Event
}{
"nil body": {
body: nil,

expectedCode: http.StatusBadRequest,
expectedResponseContains: "request without body not supported",
expectedExtensions: expectedExtensionsBase,
},

"arbitrary message": {
body: read("arbitrary message"),

expectedCode: http.StatusOK,
expectedEventData: "arbitrary message",
expectedExtensions: expectedExtensionsBase,
},

"basic auth no header": {
body: read("arbitrary message"),
username: "foo",
password: "bar",

expectedCode: http.StatusBadRequest,
expectedResponseContains: "wrong authentication header",
expectedExtensions: expectedExtensionsBase,
},

"basic auth wrong header": {
body: read("arbitrary message"),
headers: map[string]string{
"Authorization": "wrong auth",
},

username: "foo",
password: "bar",

expectedCode: http.StatusBadRequest,
expectedResponseContains: "wrong authentication header",
expectedExtensions: expectedExtensionsBase,
},

"basic auth wrong creds": {
body: read("arbitrary message"),
headers: map[string]string{
"Authorization": basicAuth("boo", "far"),
},

username: "foo",
password: "bar",

expectedCode: http.StatusUnauthorized,
expectedResponseContains: "credentials are not valid",
expectedExtensions: expectedExtensionsBase,
},

"basic auth success": {
body: read("arbitrary message"),
headers: map[string]string{
"Authorization": basicAuth("foo", "bar"),
},

username: "foo",
password: "bar",

expectedCode: http.StatusOK,
expectedEventData: "arbitrary message",
expectedExtensions: expectedExtensionsBase,
},

"extra headers": {
body: read("arbitrary message"),
headers: map[string]string{
"k1": "v1",
"k2": "v2",
},

expectedCode: http.StatusOK,
expectedEventData: "arbitrary message",
expectedExtensions: expectedExtensions(map[string]string{
"hk1": "v1",
"hk2": "v2",
}),
},

"extra queries": {
body: read("arbitrary message"),
query: "?k1=v1&k2=v2",

expectedCode: http.StatusOK,
expectedEventData: "arbitrary message",
expectedExtensions: expectedExtensions(map[string]string{
"qk1": "v1",
"qk2": "v2",
}),
},

"empty response": {
body: read("arbitrary message"),
responseEvent: newEvent(""),

expectedCode: http.StatusNoContent,
expectedEventData: "arbitrary message",
expectedExtensions: expectedExtensionsBase,
},
}

for name, c := range tc {
t.Run(name, func(t *testing.T) {
replierFn := func(inMessage event.Event) (*event.Event, protocol.Result) {
// If the test case does not define a response event use a default one
e := c.responseEvent
if e == nil {
e = newEvent(`{"test":"default"}`)
}

// If the test case does not define a result return ACK
r := c.responseResult
if r == nil {
r = protocol.ResultACK
}
return e, r
}
ceClient, chEvent := cloudeventst.NewMockRequesterClient(t, 1, replierFn, cloudevents.WithTimeNow(), cloudevents.WithUUIDs())
handler := &webhookHandler{
eventType: tEventType,
eventSource: tEventSource,
username: c.username,
password: c.password,

ceClient: ceClient,
logger: logger,
extensionAttributesFrom: &ExtensionAttributesFrom{
method: true,
path: true,
host: true,
queries: true,
headers: true,
},
}

req, _ := http.NewRequest(http.MethodGet, "/"+c.query, c.body)

Check failure on line 197 in pkg/sources/adapter/webhooksource/webhook_test.go

View workflow job for this annotation

GitHub Actions / lint

should rewrite http.NewRequestWithContext or add (*Request).WithContext (noctx)
for k, v := range c.headers {
req.Header.Add(k, v)
}
req.Host = tHost

ctx := context.Background()

th := http.HandlerFunc(handler.handleAll(ctx))

Check failure on line 205 in pkg/sources/adapter/webhooksource/webhook_test.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary conversion (unconvert)

rr := httptest.NewRecorder()

th.ServeHTTP(rr, req)

assert.Equal(t, c.expectedCode, rr.Code, "unexpected response code")
assert.Contains(t, rr.Body.String(), c.expectedResponseContains, "could not find expected response")
if c.expectedEventData != "" {
select {
case event := <-chEvent:
assert.Equal(t, c.expectedEventData, string(event.Data()), "event Data does not match")
assert.Equal(t, c.expectedExtensions, event.Context.GetExtensions(), "event extensions does not match")

case <-time.After(1 * time.Second):
assert.Fail(t, "expected cloud event containing %q was not sent", c.expectedEventData)
}
}
})
}
}

func read(s string) io.Reader {
return strings.NewReader(s)
}

func basicAuth(user, password string) string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
}

func TestAttributeSanitize(t *testing.T) {
tc := map[string]struct {
name string
sanitized string
}{
"no change": {
name: "myattribute",
sanitized: "myattribute",
},
"truncate more than 20 chars": {
name: "123456789012345678901",
sanitized: "12345678901234567890",
},
"upper case": {
name: "1A2B3c4d",
sanitized: "1a2b3c4d",
},
"non valid chars": {
name: "*-?*abcd",
sanitized: "abcd",
},
"reserved word data": {
name: "data",
sanitized: "data0",
},
"reserved word data upper case": {
name: "DatA",
sanitized: "data0",
},
"more than 20 chars, some non valid, some upper case": {
name: "1234567890*?'abcdeº!FGHIJxxxx?",
sanitized: "1234567890abcdefghij",
},
}

for name, c := range tc {
t.Run(name, func(t *testing.T) {
r := sanitizeCloudEventAttributeName(c.name)
assert.Equal(t, c.sanitized, r)
})
}
}

func expectedExtensions(extensions map[string]string) map[string]interface{} {
ee := make(map[string]interface{}, len(expectedExtensionsBase)+len(extensions))
// copy the base expected extensions
for k, v := range expectedExtensionsBase {
ee[k] = v
}
// extend or overwrite with the extensions provided for the test
for k, v := range extensions {
ee[k] = v
}
return ee
}

func newEvent(body string) *event.Event {
e := event.New(event.CloudEventsVersionV1)
e.SetType(tResponseEventType)
e.SetSource(tResponseEventSource)

if body != "" {
if err := e.SetData("text/json", body); err != nil {
panic(err)
}
}

return &e
}

0 comments on commit b8f6295

Please sign in to comment.