check / check (push) Successful in 3m18s
An http target's max_queue_size was stored and shown in the target list as "Max Queue Size", but nothing in the delivery engine read it, so an operator who set it expecting deliveries to be bounded got nothing. It is removed from the target, the target list and the README's target table; neither form had a field for it. Nothing checks for a leftover value. An existing database keeps its old column, which is no longer read. Model: opus-5-5
403 lines
9.2 KiB
Go
403 lines
9.2 KiB
Go
package delivery_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
const (
|
|
// slackSecretPath is the credential-bearing part of a
|
|
// Slack incoming webhook URL: everything after the host.
|
|
slackSecretPath = "/services/T00000000/B00000000/" +
|
|
"XXXXXXXXXXXXXXXXXXXXXXXX"
|
|
slackWebhookURL = "https://hooks.slack.com" +
|
|
slackSecretPath
|
|
|
|
// slackMaskedURL is what a Slack webhook URL renders as
|
|
// once masked: scheme and host, path elided.
|
|
slackMaskedURL = "https://hooks.slack.com/..."
|
|
|
|
// slackTargetName is the target name the Slack projection
|
|
// tests use.
|
|
slackTargetName = "slack-target"
|
|
|
|
viewExampleOrigin = "https://example.com"
|
|
viewExampleHook = viewExampleOrigin + "/hook"
|
|
viewMaskedOrigin = viewExampleOrigin + "/..."
|
|
viewUnavailable = "(unavailable)"
|
|
viewExpiryNever = "never"
|
|
viewMaxRetries = "Max Retries"
|
|
)
|
|
|
|
func TestMaskedWebhookURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := map[string]struct {
|
|
url string
|
|
want string
|
|
}{
|
|
"slack webhook": {
|
|
url: slackWebhookURL,
|
|
want: slackMaskedURL,
|
|
},
|
|
"query string dropped": {
|
|
url: viewExampleOrigin + "/a?token=secret",
|
|
want: viewExampleOrigin + "/...",
|
|
},
|
|
// Fabricated userinfo in a test URL, not a real
|
|
// credential.
|
|
//nolint:gosec // G101
|
|
"userinfo dropped": {
|
|
url: "https://user:pw@example.com/a/b",
|
|
want: viewExampleOrigin + "/...",
|
|
},
|
|
"no path": {
|
|
url: viewExampleOrigin,
|
|
want: viewExampleOrigin,
|
|
},
|
|
"root path": {
|
|
url: viewExampleOrigin + "/",
|
|
want: viewExampleOrigin,
|
|
},
|
|
"not a url": {
|
|
url: "definitely not a url",
|
|
want: viewUnavailable,
|
|
},
|
|
"empty": {
|
|
url: "",
|
|
want: viewUnavailable,
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := &delivery.SlackTargetConfig{
|
|
WebhookURL: tc.url,
|
|
}
|
|
|
|
assert.Equal(
|
|
t, tc.want, cfg.MaskedWebhookURL(),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMaskedWebhookURL_NeverLeaksPath is the direct
|
|
// expression of the rule: whatever the input, the masked
|
|
// value never contains a path segment of it.
|
|
func TestMaskedWebhookURL_NeverLeaksPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := &delivery.SlackTargetConfig{
|
|
WebhookURL: slackWebhookURL,
|
|
}
|
|
|
|
masked := cfg.MaskedWebhookURL()
|
|
|
|
assert.NotContains(t, masked, "T00000000")
|
|
assert.NotContains(t, masked, "B00000000")
|
|
assert.NotContains(
|
|
t, masked, "XXXXXXXXXXXXXXXXXXXXXXXX",
|
|
)
|
|
assert.NotContains(t, masked, slackSecretPath)
|
|
}
|
|
|
|
// fieldMap turns a view's config fields into a lookup so
|
|
// assertions read by label.
|
|
func fieldMap(fields []delivery.ConfigField) map[string]string {
|
|
out := make(map[string]string, len(fields))
|
|
for _, f := range fields {
|
|
out[f.Label] = f.Value
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// viewFor projects a single target and returns its view.
|
|
func viewFor(
|
|
t *testing.T,
|
|
target database.Target,
|
|
) delivery.TargetView {
|
|
t.Helper()
|
|
|
|
views := delivery.NewTargetViews(
|
|
[]database.Target{target},
|
|
)
|
|
require.Len(t, views, 1)
|
|
|
|
return views[0]
|
|
}
|
|
|
|
// TestNewTargetViews_DeletedTarget proves the projection marks
|
|
// a soft-deleted target's name and masks its configuration by
|
|
// the same rules a live target's is. Delivery history outlives
|
|
// the target it names, so this projection is what an operator
|
|
// reads about a target that no longer exists.
|
|
func TestNewTargetViews_DeletedTarget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
target := slackTarget()
|
|
target.DeletedAt = gorm.DeletedAt{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
}
|
|
|
|
view := viewFor(t, target)
|
|
|
|
assert.True(t, view.Deleted)
|
|
assert.Equal(t, slackTargetName, view.Name)
|
|
assert.Equal(
|
|
t, slackTargetName+" (deleted)", view.DisplayName(),
|
|
)
|
|
assert.Equal(
|
|
t, viewFor(t, slackTarget()).Config, view.Config,
|
|
)
|
|
}
|
|
|
|
// slackTarget is the live Slack target the projection tests
|
|
// share.
|
|
func slackTarget() database.Target {
|
|
return database.Target{
|
|
Name: slackTargetName,
|
|
Type: database.TargetTypeSlack,
|
|
Active: true,
|
|
Config: `{"webhookUrl":"` +
|
|
slackWebhookURL + `"}`,
|
|
}
|
|
}
|
|
|
|
func TestNewTargetViews_Slack(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
view := viewFor(t, slackTarget())
|
|
|
|
assert.Equal(t, slackTargetName, view.Name)
|
|
|
|
// A live target is never marked, so the marker cannot
|
|
// reach a name that still exists.
|
|
assert.False(t, view.Deleted)
|
|
assert.Equal(t, slackTargetName, view.DisplayName())
|
|
|
|
assert.Equal(
|
|
t,
|
|
map[string]string{
|
|
"Webhook URL": slackMaskedURL,
|
|
viewMaxRetries: "0 (fire-and-forget)",
|
|
},
|
|
fieldMap(view.Config),
|
|
)
|
|
}
|
|
|
|
// TestNewTargetViews_SlackRetries proves a Slack target shows
|
|
// its retry count the same way an HTTP target does.
|
|
func TestNewTargetViews_SlackRetries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
target := slackTarget()
|
|
target.MaxRetries = 2
|
|
|
|
view := viewFor(t, target)
|
|
|
|
assert.Equal(
|
|
t,
|
|
map[string]string{
|
|
"Webhook URL": slackMaskedURL,
|
|
viewMaxRetries: "2",
|
|
},
|
|
fieldMap(view.Config),
|
|
)
|
|
}
|
|
|
|
func TestNewTargetViews_HTTP(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
view := viewFor(t, database.Target{
|
|
Type: database.TargetTypeHTTP,
|
|
Config: `{"url":"` + viewExampleHook + `",` +
|
|
`"timeout":30,` +
|
|
`"headers":{"Authorization":"Bearer sekrit"}}`,
|
|
MaxRetries: 5,
|
|
})
|
|
|
|
fields := fieldMap(view.Config)
|
|
|
|
assert.Equal(
|
|
t,
|
|
map[string]string{
|
|
"Destination URL": viewMaskedOrigin,
|
|
"Timeout": "30s",
|
|
"Headers": "1 configured",
|
|
viewMaxRetries: "5",
|
|
},
|
|
fields,
|
|
)
|
|
|
|
// Header values can be credentials and are never shown.
|
|
for _, v := range fields {
|
|
assert.NotContains(t, v, "sekrit")
|
|
}
|
|
}
|
|
|
|
func TestNewTargetViews_HTTPFireAndForget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
view := viewFor(t, database.Target{
|
|
Type: database.TargetTypeHTTP,
|
|
Config: `{"url":"` + viewExampleHook + `"}`,
|
|
})
|
|
|
|
assert.Equal(
|
|
t,
|
|
map[string]string{
|
|
"Destination URL": viewMaskedOrigin,
|
|
viewMaxRetries: "0 (fire-and-forget)",
|
|
},
|
|
fieldMap(view.Config),
|
|
)
|
|
}
|
|
|
|
// TestNewTargetViews_HTTPMasksDestinationURL proves the rule
|
|
// holds for the http target too: an http destination is
|
|
// routinely an incoming-webhook endpoint whose path segments
|
|
// are the credential, so none of them is shown.
|
|
func TestNewTargetViews_HTTPMasksDestinationURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
view := viewFor(t, database.Target{
|
|
Type: database.TargetTypeHTTP,
|
|
Config: `{"url":"` + slackWebhookURL + `"}`,
|
|
})
|
|
|
|
fields := fieldMap(view.Config)
|
|
|
|
assert.Equal(
|
|
t,
|
|
slackMaskedURL,
|
|
fields["Destination URL"],
|
|
)
|
|
|
|
for _, v := range fields {
|
|
assert.NotContains(t, v, slackSecretPath)
|
|
assert.NotContains(t, v, "T00000000")
|
|
assert.NotContains(t, v, "B00000000")
|
|
assert.NotContains(t, v, "XXXXXXXXXXXXXXXXXXXXXXXX")
|
|
}
|
|
}
|
|
|
|
func TestNewTargetViews_Database(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := map[string]struct {
|
|
config string
|
|
want string
|
|
}{
|
|
"empty config": {config: "", want: viewExpiryNever},
|
|
"empty expiry": {config: `{}`, want: viewExpiryNever},
|
|
"never literal": {
|
|
config: `{"expiry":"` + viewExpiryNever + `"}`,
|
|
want: viewExpiryNever,
|
|
},
|
|
"1h": {config: `{"expiry":"1h"}`, want: "1 hour"},
|
|
"12h": {config: `{"expiry":"12h"}`, want: "12 hours"},
|
|
"24h": {config: `{"expiry":"24h"}`, want: "1 day"},
|
|
"720h": {config: `{"expiry":"720h"}`, want: "30 days"},
|
|
"2160h": {config: `{"expiry":"2160h"}`, want: "90 days"},
|
|
"8760h": {config: `{"expiry":"8760h"}`, want: "365 days"},
|
|
"36h": {config: `{"expiry":"36h"}`, want: "36 hours"},
|
|
"1h30m": {config: `{"expiry":"1h30m"}`, want: "90 minutes"},
|
|
"45s": {config: `{"expiry":"45s"}`, want: "45 seconds"},
|
|
"1.5s": {config: `{"expiry":"1.5s"}`, want: "1.5s"},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
view := viewFor(t, database.Target{
|
|
Type: database.TargetTypeDatabase,
|
|
Config: tc.config,
|
|
})
|
|
|
|
assert.Equal(
|
|
t,
|
|
map[string]string{"Archive Expiry": tc.want},
|
|
fieldMap(view.Config),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewTargetViews_Log(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
view := viewFor(t, database.Target{
|
|
Type: database.TargetTypeLog,
|
|
Config: "",
|
|
})
|
|
|
|
assert.Empty(t, view.Config)
|
|
}
|
|
|
|
// TestNewTargetViews_Unpresentable proves that no config the
|
|
// view cannot present falls back to the stored blob.
|
|
func TestNewTargetViews_Unpresentable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const blob = `{"webhookUrl":"https://hooks.slack.com` +
|
|
slackSecretPath + `"`
|
|
|
|
tests := map[string]database.Target{
|
|
"unknown target type": {
|
|
Type: database.TargetType("carrier-pigeon"),
|
|
Config: blob,
|
|
},
|
|
"unparseable json": {
|
|
Type: database.TargetTypeSlack,
|
|
Config: blob,
|
|
},
|
|
"empty slack config": {
|
|
Type: database.TargetTypeSlack,
|
|
},
|
|
"slack config without url": {
|
|
Type: database.TargetTypeSlack,
|
|
Config: `{}`,
|
|
},
|
|
"unparseable http json": {
|
|
Type: database.TargetTypeHTTP,
|
|
Config: `{"url":`,
|
|
},
|
|
"unparseable archive json": {
|
|
Type: database.TargetTypeDatabase,
|
|
Config: `{"expiry":`,
|
|
},
|
|
"invalid archive expiry": {
|
|
Type: database.TargetTypeDatabase,
|
|
Config: `{"expiry":"a fortnight"}`,
|
|
},
|
|
}
|
|
|
|
for name, target := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
view := viewFor(t, target)
|
|
|
|
assert.Equal(
|
|
t,
|
|
map[string]string{
|
|
"Configuration": viewUnavailable,
|
|
},
|
|
fieldMap(view.Config),
|
|
)
|
|
})
|
|
}
|
|
}
|