Name a deleted target on its historical deliveries (closes #211)
All checks were successful
check / check (push) Successful in 3m13s

This commit was merged in pull request #266.
This commit is contained in:
2026-08-24 02:03:23 +02:00
parent 763d8f8058
commit 5fda446c71
5 changed files with 256 additions and 31 deletions

View File

@@ -23,21 +23,51 @@ type ConfigField struct {
Value string
}
// deletedNameSuffix marks the name of a target that no longer
// exists. Deletes are soft and delivery history outlives the
// target, so the event log shows names of targets that are gone;
// an operator reading one needs to know it cannot be delivered
// to, replayed to, or configured.
const deletedNameSuffix = " (deleted)"
// TargetView is the display-safe projection of a target for
// the UI. It deliberately has no raw configuration field, so
// no template — present or future — can render the stored
// blob.
type TargetView struct {
ID string
Name string
ID string
Name string
// Deleted reports that this target's row is soft deleted.
// Only views built for historical display carry it set:
// every other projection is of a live row.
Deleted bool
Type database.TargetType
Active bool
Config []ConfigField
}
// DisplayName is the name to render, marked when the target has
// been deleted. Templates showing a name against historical data
// must use it rather than Name, which stays the stored name.
func (v TargetView) DisplayName() string {
if v.Deleted {
return v.Name + deletedNameSuffix
}
return v.Name
}
// NewTargetViews projects targets for rendering, replacing
// each stored configuration blob with named, display-safe
// fields.
//
// A soft-deleted row projects exactly as a live one does, minus
// the deleted marker on its name: masking is a property of the
// projection, not of the row's state, so a deleted target's
// credential is as unreachable from a template as a live
// target's.
func NewTargetViews(
targets []database.Target,
) []TargetView {
@@ -47,11 +77,12 @@ func NewTargetViews(
t := &targets[i]
views = append(views, TargetView{
ID: t.ID,
Name: t.Name,
Type: t.Type,
Active: t.Active,
Config: targetConfigFields(t),
ID: t.ID,
Name: t.Name,
Deleted: t.DeletedAt.Valid,
Type: t.Type,
Active: t.Active,
Config: targetConfigFields(t),
})
}

View File

@@ -2,9 +2,11 @@ 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"
)
@@ -17,6 +19,14 @@ const (
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 + "/..."
@@ -33,7 +43,7 @@ func TestMaskedWebhookURL(t *testing.T) {
}{
"slack webhook": {
url: slackWebhookURL,
want: "https://hooks.slack.com/...",
want: slackMaskedURL,
},
"query string dropped": {
url: viewExampleOrigin + "/a?token=secret",
@@ -125,23 +135,61 @@ func viewFor(
return views[0]
}
func TestNewTargetViews_Slack(t *testing.T) {
// 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()
view := viewFor(t, database.Target{
Name: "slack-target",
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,
map[string]string{"Webhook URL": slackMaskedURL},
fieldMap(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, "slack-target", view.Name)
assert.Equal(
t,
map[string]string{
"Webhook URL": "https://hooks.slack.com/...",
},
map[string]string{"Webhook URL": slackMaskedURL},
fieldMap(view.Config),
)
}
@@ -212,7 +260,7 @@ func TestNewTargetViews_HTTPMasksDestinationURL(t *testing.T) {
assert.Equal(
t,
"https://hooks.slack.com/...",
slackMaskedURL,
fields["Destination URL"],
)