Store a target's settings as columns (closes #543)
check / check (push) Waiting to run

A target's settings move from the per-type JSON string in
targets.config to columns: url, headers, timeout_seconds,
forward_query, archive_expiry and archive_rotation. A column a type
does not use stays empty. headers holds the header map as JSON text
through GORM's JSON serializer, and archive_expiry holds a
time.Duration, 0 meaning never, so nothing that reads a target parses
it. The forms check each setting once, with the same refusals and
messages, and the edit form is filled from the columns, so the
per-type config structs, their parsers and the "could not be read"
branches are gone. A delivery task carries the six settings in place
of the JSON string.

Model: opus-5-5
This commit is contained in:
2026-10-15 19:54:42 +00:00
parent 0cdda43e6c
commit 82bab57dfe
69 changed files with 1404 additions and 2159 deletions
+62 -86
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/stretchr/testify/assert"
@@ -25,25 +26,24 @@ const (
slackSecretPath
)
// seedConfiguredTarget inserts a target with a stored config
// blob and returns it.
// seedConfiguredTarget inserts an active target of targetType with
// the settings (URL to ArchiveRotation) that settings carries, and
// returns it.
func seedConfiguredTarget(
t *testing.T,
db *database.Database,
webhookID string,
targetType database.TargetType,
config string,
settings database.Target,
) *database.Target {
t.Helper()
tgt := &database.Target{
WebhookID: webhookID,
Name: "t-" + string(targetType),
Type: targetType,
Active: true,
Config: config,
MaxAttempts: 1,
}
tgt := &settings
tgt.WebhookID = webhookID
tgt.Name = "t-" + string(targetType)
tgt.Type = targetType
tgt.Active = true
tgt.MaxAttempts = 1
require.NoError(
t,
@@ -108,89 +108,59 @@ func serveSourceDetailPage(
return w
}
// TestHandleSourceDetail_MasksSlackWebhookURL is the
// load-bearing regression test for the credential leak: the
// rendered page must show the Slack target without any of the
// secret path segments of its webhook URL.
func TestHandleSourceDetail_MasksSlackWebhookURL(t *testing.T) {
// TestHandleSourceDetail_MasksTargetURL is the load-bearing
// regression test for the credential leak: the rendered page must
// show a Slack target, and an http target, without any of the secret
// path segments of its URL. An http target's destination is routinely
// an incoming-webhook endpoint too, so the same Slack URL serves for
// both.
func TestHandleSourceDetail_MasksTargetURL(t *testing.T) {
t.Parallel()
var (
h *handlers.Handlers
sess *session.Session
db *database.Database
)
labels := map[database.TargetType]string{
database.TargetTypeSlack: "Webhook URL",
database.TargetTypeHTTP: "Destination URL",
}
app := newTestApp(t, &h, &sess, &db)
app.RequireStart()
for targetType, label := range labels {
t.Run(string(targetType), func(t *testing.T) {
t.Parallel()
t.Cleanup(app.RequireStop)
var (
h *handlers.Handlers
sess *session.Session
db *database.Database
)
wh := seedWebhook(t, db)
seedConfiguredTarget(
t, db, wh.ID,
database.TargetTypeSlack,
`{"webhookUrl":"`+slackWebhookURL+`"}`,
)
app := newTestApp(t, &h, &sess, &db)
app.RequireStart()
body := renderSourceDetailPage(t, h, sess, wh.ID)
t.Cleanup(app.RequireStop)
assert.NotContains(t, body, slackSecretPath)
assert.NotContains(t, body, "T00000000")
assert.NotContains(t, body, "B00000000")
assert.NotContains(
t, body, "XXXXXXXXXXXXXXXXXXXXXXXX",
)
assert.NotContains(t, body, "webhookUrl")
wh := seedWebhook(t, db)
seedConfiguredTarget(
t, db, wh.ID, targetType,
database.Target{URL: slackWebhookURL},
)
assert.Contains(t, body, "Webhook URL")
assert.Contains(t, body, "https://hooks.slack.com/...")
}
body := renderSourceDetailPage(t, h, sess, wh.ID)
// TestHandleSourceDetail_MasksHTTPDestinationURL is the
// regression test for the same leak reached through the http
// target: its destination is routinely an incoming-webhook
// endpoint whose path segments are the credential, so the
// rendered page must not contain them.
func TestHandleSourceDetail_MasksHTTPDestinationURL(
t *testing.T,
) {
t.Parallel()
assert.NotContains(t, body, slackSecretPath)
assert.NotContains(t, body, "T00000000")
assert.NotContains(t, body, "B00000000")
assert.NotContains(
t, body, "XXXXXXXXXXXXXXXXXXXXXXXX",
)
var (
h *handlers.Handlers
sess *session.Session
db *database.Database
)
app := newTestApp(t, &h, &sess, &db)
app.RequireStart()
t.Cleanup(app.RequireStop)
wh := seedWebhook(t, db)
seedConfiguredTarget(
t, db, wh.ID,
database.TargetTypeHTTP,
`{"url":"`+slackWebhookURL+`"}`,
)
body := renderSourceDetailPage(t, h, sess, wh.ID)
assert.NotContains(t, body, slackSecretPath)
assert.NotContains(t, body, "T00000000")
assert.NotContains(t, body, "B00000000")
assert.NotContains(
t, body, "XXXXXXXXXXXXXXXXXXXXXXXX",
)
assert.Contains(t, body, "Destination URL")
assert.Contains(t, body, "https://hooks.slack.com/...")
assert.Contains(t, body, label)
assert.Contains(t, body, "https://hooks.slack.com/...")
})
}
}
// TestHandleSourceDetail_RendersNamedTargetFields proves the
// other target types render labelled fields rather than the
// stored blob.
// stored values.
func TestHandleSourceDetail_RendersNamedTargetFields(
t *testing.T,
) {
@@ -212,18 +182,24 @@ func TestHandleSourceDetail_RendersNamedTargetFields(
seedConfiguredTarget(
t, db, wh.ID,
database.TargetTypeHTTP,
`{"url":"https://example.com/hook","timeout":30,`+
`"headers":{"Authorization":"Bearer sekrit"}}`,
database.Target{
URL: "https://example.com/hook",
TimeoutSeconds: 30,
Headers: map[string]string{"X-Api-Key": "sekrit"},
},
)
seedConfiguredTarget(
t, db, wh.ID,
database.TargetTypeDatabase,
`{"expiry":"720h"}`,
database.Target{
ArchiveExpiry: 720 * time.Hour,
ArchiveRotation: rotationNone,
},
)
seedConfiguredTarget(
t, db, wh.ID,
database.TargetType("carrier-pigeon"),
`{"beak":"sharp"}`,
database.Target{URL: "https://pigeon.example/beak"},
)
body := renderSourceDetailPage(t, h, sess, wh.ID)
@@ -242,8 +218,8 @@ func TestHandleSourceDetail_RendersNamedTargetFields(
assert.Contains(t, list, "Archive rotation: none")
assert.Contains(t, body, `<option value="database">Archive</option>`)
// An unknown type gets the neutral placeholder, never the
// stored blob.
// An unknown type gets the neutral placeholder, never its
// stored URL.
assert.Contains(t, body, "(unavailable)")
assert.NotContains(t, body, "beak")
}