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. 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; tests check that each copy
into a task, and back out of one, keeps all six.

Model: opus-5-5
This commit was merged in pull request #554.
This commit is contained in:
2026-10-15 23:02:39 +02:00
parent ded39aa158
commit 8406c77949
71 changed files with 1665 additions and 2168 deletions
+51 -59
View File
@@ -106,10 +106,10 @@ func (env *archiveEnv) archivePath(tgt *database.Target) string {
)
}
// seedDatabaseTarget creates a webhook with one database target
// carrying the given target config JSON, and returns the target.
// seedDatabaseTarget creates a webhook with one database target with
// the given archive expiry and rotation, and returns the target.
func (env *archiveEnv) seedDatabaseTarget(
t *testing.T, configJSON string,
t *testing.T, expiry time.Duration, rotation string,
) *database.Target {
t.Helper()
@@ -124,23 +124,27 @@ func (env *archiveEnv) seedDatabaseTarget(
Create(wh).Error,
)
return env.addDatabaseTarget(t, wh.ID, configJSON)
return env.addDatabaseTarget(t, wh.ID, expiry, rotation)
}
// addDatabaseTarget creates one more database target on an
// existing webhook and returns it.
func (env *archiveEnv) addDatabaseTarget(
t *testing.T, webhookID, configJSON string,
t *testing.T,
webhookID string,
expiry time.Duration,
rotation string,
) *database.Target {
t.Helper()
tgt := &database.Target{
WebhookID: webhookID,
Name: "Archive",
Type: database.TargetTypeDatabase,
Active: true,
Config: configJSON,
MaxAttempts: 1,
WebhookID: webhookID,
Name: "Archive",
Type: database.TargetTypeDatabase,
Active: true,
ArchiveExpiry: expiry,
ArchiveRotation: rotation,
MaxAttempts: 1,
}
require.NoError(
t,
@@ -323,7 +327,7 @@ func TestArchiveSweeper_LoopOutlivesStartHookContext(
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
now := time.Now()
path := env.seedArchiveRows(
@@ -377,7 +381,7 @@ func TestArchiveSweep_DoesNotResurrectEvictedWriter(
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
env.seedArchiveRows(
t, tgt, time.Now().Add(-48*time.Hour),
)
@@ -411,7 +415,7 @@ func TestArchiveSweep_LeavesNoRegistryEntry(t *testing.T) {
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
path := env.seedArchiveRows(
t, tgt,
time.Now().Add(-48*time.Hour),
@@ -444,7 +448,7 @@ func TestArchiveSweep_KeepsWriterAdoptedByDelivery(
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
env.seedArchiveRows(
t, tgt, time.Now().Add(-48*time.Hour),
)
@@ -498,7 +502,7 @@ func TestArchiveSweep_KeepsWriterAdoptedDuringSweep(
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
env.seedArchiveRows(
t, tgt, time.Now().Add(-48*time.Hour),
)
@@ -551,8 +555,8 @@ func TestArchiveSweep_KeepsWriterAdoptedDuringSweep(
// TestArchiveSweep_ContinuesAfterPerTargetFailure proves a
// failure for one target does not abort the sweep for the
// others: an unparseable expiry and an unreadable archive both
// have to be logged and stepped over.
// others: an unreadable archive has to be logged and stepped
// over.
func TestArchiveSweep_ContinuesAfterPerTargetFailure(
t *testing.T,
) {
@@ -560,21 +564,16 @@ func TestArchiveSweep_ContinuesAfterPerTargetFailure(
env := setupArchiveTest(t)
// Seeded first so the sweep reaches them before the healthy
// Seeded first so the sweep reaches it before the healthy
// target: targets come back in insertion order.
badConfig := env.seedDatabaseTarget(t, `{"expiry":"!!!"}`)
env.seedArchiveRows(
t, badConfig, time.Now().Add(-48*time.Hour),
)
corrupt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
corrupt := env.seedDatabaseTarget(t, time.Hour, "")
require.NoError(t, os.WriteFile(
env.archivePath(corrupt),
[]byte("this is not a sqlite database"),
0o600,
))
healthy := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
healthy := env.seedDatabaseTarget(t, time.Hour, "")
healthyPath := env.seedArchiveRows(
t, healthy,
time.Now().Add(-48*time.Hour),
@@ -631,7 +630,7 @@ func TestArchiveSweep_PrunesIdleArchive(t *testing.T) {
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
now := time.Now()
path := env.seedArchiveRows(
@@ -670,7 +669,7 @@ func TestArchiveSweep_LeavesArchiveClosed(t *testing.T) {
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
path := env.seedArchiveRows(
t, tgt, time.Now().Add(-48*time.Hour),
)
@@ -710,7 +709,7 @@ func TestArchiveSweep_ClosesHandleOfRegisteredWriter(
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
env.seedArchiveRows(
t, tgt, time.Now().Add(-48*time.Hour),
)
@@ -795,36 +794,29 @@ func TestArchiveSweep_CancelledSweepLogsNoError(t *testing.T) {
}
// TestArchiveSweep_NeverExpiryUntouched proves the sweep is a
// no-op for the default retention policy, so archives with no
// expiry (or the literal "never") behave exactly as before.
// no-op for the default retention policy, so an archive with an
// expiry of 0, which keeps it forever, is left alone.
func TestArchiveSweep_NeverExpiryUntouched(t *testing.T) {
t.Parallel()
for _, configJSON := range []string{
`{"expiry":"never"}`,
`{"expiry":""}`,
"",
} {
env := setupArchiveTest(t)
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, configJSON)
path := env.seedArchiveRows(
t, tgt,
time.Now().Add(-10000*time.Hour),
)
tgt := env.seedDatabaseTarget(t, 0, "")
path := env.seedArchiveRows(
t, tgt,
time.Now().Add(-10000*time.Hour),
)
env.sweeper.ExportSweep(context.Background())
env.sweeper.ExportSweep(context.Background())
assert.Equal(
t, []string{sweepRowOld}, archivedEventIDs(t, path),
"config %q must keep rows forever", configJSON,
)
assert.False(
t, env.writers.ExportHasArchiveWriter(tgt.ID),
"config %q must leave no registry entry behind",
configJSON,
)
}
assert.Equal(
t, []string{sweepRowOld}, archivedEventIDs(t, path),
"an expiry of 0 must keep rows forever",
)
assert.False(
t, env.writers.ExportHasArchiveWriter(tgt.ID),
"an expiry of 0 must leave no registry entry behind",
)
}
// TestArchiveSweep_NeverExpirySkipsBeforeOpening pins the
@@ -845,7 +837,7 @@ func TestArchiveSweep_NeverExpirySkipsBeforeOpening(
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"never"}`)
tgt := env.seedDatabaseTarget(t, 0, "")
path := env.archivePath(tgt)
seedUnmigratedArchive(t, path)
@@ -897,7 +889,7 @@ func TestArchiveSweep_DoesNotCreateArchiveFile(t *testing.T) {
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
path := env.archivePath(tgt)
require.NoFileExists(t, path)
@@ -928,7 +920,7 @@ func TestArchiveSweep_DoesNotCreateAfterWriterExists(
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
path, err := env.writers.ExportEnsureArchiveWriter(tgt.ID)
require.NoError(t, err)
@@ -947,7 +939,7 @@ func TestArchiveSweep_SkipsDeletedWebhookTargets(t *testing.T) {
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
path := env.seedArchiveRows(
t, tgt, time.Now().Add(-48*time.Hour),
)
@@ -975,7 +967,7 @@ func TestArchiveSweep_ConcurrentWrites(t *testing.T) {
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
deliveries := make(
[]*database.Delivery, 0, sweepConcurrentWrites,
@@ -1020,7 +1012,7 @@ func TestArchiveSweeper_StopsCleanly(t *testing.T) {
env := setupArchiveTest(t)
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
tgt := env.seedDatabaseTarget(t, time.Hour, "")
env.seedArchiveRows(
t, tgt, time.Now().Add(-48*time.Hour),
)