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" viewMaxAttempts = "Delivery attempts" ) // The archive rotations a database target's settings can name. const ( rotationNone = "none" rotationMonthly = "monthly" rotationDaily = "daily" rotationHourly = "hourly" ) // TestNewTargetViews_SlackMasksWebhookURL shows what a Slack target's // URL renders as on the target list. func TestNewTargetViews_SlackMasksWebhookURL(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() target := slackTarget() target.URL = tc.url assert.Equal( t, tc.want, fieldMap(viewFor(t, target).Config)["Webhook URL"], ) }) } } // TestNewTargetViews_SlackNeverLeaksPath is the direct // expression of the rule: whatever the input, the masked // value never contains a path segment of it. func TestNewTargetViews_SlackNeverLeaksPath(t *testing.T) { t.Parallel() masked := fieldMap(viewFor(t, slackTarget()).Config)["Webhook URL"] 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, URL: slackWebhookURL, MaxAttempts: 1, } } 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, viewMaxAttempts: "1", }, fieldMap(view.Config), ) } // TestNewTargetViews_SlackAttempts proves a Slack target shows // its number of delivery attempts the same way an HTTP target // does. func TestNewTargetViews_SlackAttempts(t *testing.T) { t.Parallel() target := slackTarget() target.MaxAttempts = 2 view := viewFor(t, target) assert.Equal( t, map[string]string{ "Webhook URL": slackMaskedURL, viewMaxAttempts: "2", }, fieldMap(view.Config), ) } func TestNewTargetViews_HTTP(t *testing.T) { t.Parallel() view := viewFor(t, database.Target{ Type: database.TargetTypeHTTP, URL: viewExampleHook, TimeoutSeconds: 30, Headers: map[string]string{headerAuthorization: "Bearer sekrit"}, ForwardQuery: true, MaxAttempts: 5, }) fields := fieldMap(view.Config) assert.Equal( t, map[string]string{ "Destination URL": viewMaskedOrigin, "Timeout": "30s", "Headers": "1 configured", "Query string": "passed on to this target", viewMaxAttempts: "5", }, fields, ) // Header values can be credentials and are never shown. for _, v := range fields { assert.NotContains(t, v, "sekrit") } } // TestNewTargetViews_HTTPSingleAttempt proves a single attempt is // shown as the plain number, like any other. func TestNewTargetViews_HTTPSingleAttempt(t *testing.T) { t.Parallel() view := viewFor(t, database.Target{ Type: database.TargetTypeHTTP, URL: viewExampleHook, MaxAttempts: 1, }) assert.Equal( t, map[string]string{ "Destination URL": viewMaskedOrigin, viewMaxAttempts: "1", }, 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, URL: slackWebhookURL, MaxAttempts: 1, }) 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 { expiry time.Duration want string }{ "never": {expiry: 0, want: viewExpiryNever}, "1h": {expiry: time.Hour, want: "1 hour"}, "12h": {expiry: 12 * time.Hour, want: "12 hours"}, "24h": {expiry: 24 * time.Hour, want: "1 day"}, "720h": {expiry: 720 * time.Hour, want: "30 days"}, "2160h": {expiry: 2160 * time.Hour, want: "90 days"}, "8760h": {expiry: 8760 * time.Hour, want: "365 days"}, "36h": {expiry: 36 * time.Hour, want: "36 hours"}, "1h30m": {expiry: 90 * time.Minute, want: "90 minutes"}, "45s": {expiry: 45 * time.Second, want: "45 seconds"}, "1.5s": {expiry: 1500 * time.Millisecond, 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, ArchiveExpiry: tc.expiry, ArchiveRotation: rotationNone, }) assert.Equal( t, map[string]string{ "Archive expiry": tc.want, "Archive rotation": rotationNone, }, fieldMap(view.Config), ) }) } } // TestNewTargetViews_DatabaseRotation proves the target list shows a // database target's rotation. func TestNewTargetViews_DatabaseRotation(t *testing.T) { t.Parallel() for _, rotation := range []string{ rotationNone, rotationMonthly, rotationDaily, rotationHourly, } { t.Run(rotation, func(t *testing.T) { t.Parallel() view := viewFor(t, database.Target{ Type: database.TargetTypeDatabase, ArchiveRotation: rotation, }) assert.Equal( t, rotation, fieldMap(view.Config)["Archive rotation"], ) }) } } func TestNewTargetViews_Log(t *testing.T) { t.Parallel() view := viewFor(t, database.Target{ Type: database.TargetTypeLog, }) assert.Empty(t, view.Config) } // TestNewTargetViews_UnknownType proves that a target of a type the // view does not know shows the neutral placeholder, and never its // URL. func TestNewTargetViews_UnknownType(t *testing.T) { t.Parallel() view := viewFor(t, database.Target{ Type: database.TargetType("carrier-pigeon"), URL: slackWebhookURL, }) assert.Equal( t, map[string]string{ "Configuration": viewUnavailable, }, fieldMap(view.Config), ) }