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

Deleting a target soft deletes its row while its deliveries survive
in the per-webhook database, so the event log kept rendering the
history and lost the label: every historical row read ": delivered"
and the deliveries block was headed by an empty name.

loadTargetMap already loaded soft-deleted rows Unscoped for the
redactor half, then discarded them before building the view half.
It now builds both halves from every loaded row, and TargetView
carries a Deleted flag with a DisplayName that renders
"name (deleted)" — an operator debugging an old delivery needs to
know the target is gone, not just what it was called.

The views still come from NewTargetViews, so a deleted target's
configuration is masked by exactly the code that masks a live
one's. The widening is confined to this map, which feeds only
DeliveryView.Target on the event log page: the source detail
target list, the target edit form, the receiver and resubmit fan-out
and the delivery engine each resolve targets through their own
scoped queries, and the replay path keeps refusing a deleted target.
This commit is contained in:
2026-08-23 23:49:34 +00:00
parent fd5966f807
commit 2729155f9b
5 changed files with 256 additions and 31 deletions

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"],
)