All checks were successful
check / check (push) Successful in 6m0s
Expanding a delivery on the event log page now shows each recorded attempt: attempt number, outcome, status code, duration, error and response body. Previously a failure rendered as "target: failed" and diagnosing it meant opening the per-webhook SQLite file by hand. The response body is cut by SQLite via substr over a blob cast, the same projection the event body uses, so an oversized stored response never becomes a Go string. The page reports the cut with a marker. Response bodies and errors are remote content, so both go through a new delivery.Redactor that strips the target's own destination URL, path, query and userinfo before rendering. Configured HTTP header values are deliberately not redacted; they are as often routine as secret, and replacing them would mangle ordinary responses. Target configuration keeps reaching the template only as a TargetView.
138 lines
3.6 KiB
Go
138 lines
3.6 KiB
Go
package delivery_test
|
|
|
|
import (
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
// The secret path segments of a Slack incoming webhook URL.
|
|
const (
|
|
redactSecretPath = "/services/T11111111/B11111111/" +
|
|
"YYYYYYYYYYYYYYYYYYYYYYYY"
|
|
redactWebhookURL = "https://hooks.slack.com" +
|
|
redactSecretPath
|
|
)
|
|
|
|
func TestRedactor_RemovesSlackWebhookURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := delivery.NewRedactor(&database.Target{
|
|
Type: database.TargetTypeSlack,
|
|
Config: `{"webhookUrl":"` + redactWebhookURL + `"}`,
|
|
})
|
|
|
|
got := r.Redact("no_service for " + redactWebhookURL)
|
|
|
|
assert.NotContains(t, got, redactSecretPath)
|
|
assert.NotContains(t, got, "T11111111")
|
|
assert.Contains(t, got, delivery.RedactionMarker)
|
|
assert.Contains(t, got, "no_service for ")
|
|
}
|
|
|
|
// TestRedactor_RemovesBarePath covers a remote that echoes
|
|
// only the request path rather than the whole URL. The path
|
|
// segments are the credential on their own.
|
|
func TestRedactor_RemovesBarePath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := delivery.NewRedactor(&database.Target{
|
|
Type: database.TargetTypeSlack,
|
|
Config: `{"webhookUrl":"` + redactWebhookURL + `"}`,
|
|
})
|
|
|
|
got := r.Redact("POST " + redactSecretPath + " 404")
|
|
|
|
assert.NotContains(t, got, redactSecretPath)
|
|
assert.Equal(
|
|
t,
|
|
"POST "+delivery.RedactionMarker+" 404",
|
|
got,
|
|
)
|
|
}
|
|
|
|
// TestRedactor_RemovesHTTPURLQueryAndUserinfo covers the HTTP
|
|
// target, whose destination is an arbitrary URL: the query
|
|
// string and the userinfo carry credentials as readily as the
|
|
// path does.
|
|
func TestRedactor_RemovesHTTPURLQueryAndUserinfo(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Assembled rather than written out, so the literal is
|
|
// not itself a credential-shaped string.
|
|
dest := url.URL{
|
|
Scheme: "https",
|
|
User: url.UserPassword("user", "hunter2"),
|
|
Host: "example.com",
|
|
Path: "/in",
|
|
RawQuery: "token=s3cr3t",
|
|
}
|
|
raw := dest.String()
|
|
|
|
r := delivery.NewRedactor(&database.Target{
|
|
Type: database.TargetTypeHTTP,
|
|
Config: `{"url":"` + raw + `"}`,
|
|
})
|
|
|
|
for _, echoed := range []string{
|
|
raw,
|
|
"/in?token=s3cr3t",
|
|
"hunter2",
|
|
} {
|
|
got := r.Redact("rejected: " + echoed)
|
|
|
|
assert.NotContains(t, got, "s3cr3t", echoed)
|
|
assert.NotContains(t, got, "hunter2", echoed)
|
|
assert.Contains(
|
|
t, got, delivery.RedactionMarker, echoed,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestRedactor_LeavesUnrelatedTextAlone pins that the
|
|
// redactor matches literally: it does not guess at what a
|
|
// secret looks like, so ordinary response content survives.
|
|
func TestRedactor_LeavesUnrelatedTextAlone(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const response = "ok=false error=channel_not_found"
|
|
|
|
r := delivery.NewRedactor(&database.Target{
|
|
Type: database.TargetTypeSlack,
|
|
Config: `{"webhookUrl":"` + redactWebhookURL + `"}`,
|
|
})
|
|
|
|
assert.Equal(t, response, r.Redact(response))
|
|
}
|
|
|
|
// TestRedactor_ZeroValueAndConfiglessTargets pins that a
|
|
// caller with no target, an unparseable config, or a target
|
|
// type with no destination URL gets a redactor that changes
|
|
// nothing rather than one that panics.
|
|
func TestRedactor_ZeroValueAndConfiglessTargets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const text = "some response body"
|
|
|
|
var zero delivery.Redactor
|
|
|
|
assert.Equal(t, text, zero.Redact(text))
|
|
assert.Equal(t, text, delivery.NewRedactor(nil).Redact(text))
|
|
|
|
for _, tgt := range []database.Target{
|
|
{Type: database.TargetTypeLog},
|
|
{Type: database.TargetTypeDatabase},
|
|
{Type: database.TargetTypeSlack, Config: "not json"},
|
|
{Type: database.TargetTypeHTTP, Config: ""},
|
|
} {
|
|
assert.Equal(
|
|
t, text,
|
|
delivery.NewRedactor(&tgt).Redact(text),
|
|
tgt.Type,
|
|
)
|
|
}
|
|
}
|