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:
@@ -28,7 +28,7 @@ func TestEventDBCreateOmitsAssociations(t *testing.T) {
|
||||
WebhookID: webhookID,
|
||||
Name: "leaky-target",
|
||||
Type: database.TargetTypeSlack,
|
||||
Config: `{"webhookUrl":"https://hooks.example/secret"}`,
|
||||
URL: "https://hooks.example/secret",
|
||||
MaxAttempts: 1,
|
||||
}
|
||||
target.ID = uuid.New().String()
|
||||
|
||||
@@ -29,8 +29,10 @@ func marshalModel(t *testing.T, v any) string {
|
||||
// these tags the first handler that marshals a model serialises the
|
||||
// secret with it. Each field below is a live credential:
|
||||
//
|
||||
// - Target.Config holds an incoming-webhook URL whose path segments
|
||||
// are the bearer token.
|
||||
// - Target.URL is an incoming-webhook URL whose path segments are
|
||||
// the bearer token.
|
||||
// - Target.Headers holds header values, routinely authorization
|
||||
// tokens.
|
||||
// - APIKey.Key is a bearer token outright.
|
||||
// - Setting.Value holds the session encryption key.
|
||||
// - User.Password holds the Argon2 hash, and was already tagged.
|
||||
@@ -44,11 +46,20 @@ func TestModelsDoNotMarshalTheirSecrets(t *testing.T) {
|
||||
model any
|
||||
}{
|
||||
{
|
||||
name: "target config",
|
||||
name: "target url",
|
||||
model: database.Target{
|
||||
Name: keptField,
|
||||
Type: database.TargetTypeSlack,
|
||||
Config: `{"webhookUrl":"https://h/s/` + marker + `"}`,
|
||||
URL: "https://h/s/" + marker,
|
||||
MaxAttempts: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "target headers",
|
||||
model: database.Target{
|
||||
Name: keptField,
|
||||
Type: database.TargetTypeHTTP,
|
||||
Headers: map[string]string{"Authorization": marker},
|
||||
MaxAttempts: 1,
|
||||
},
|
||||
},
|
||||
@@ -87,10 +98,10 @@ func TestModelsDoNotMarshalTheirSecrets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebhookMarshalsNoTargetConfig covers the nested case: a webhook
|
||||
// TestWebhookMarshalsNoTargetURL covers the nested case: a webhook
|
||||
// marshalled with its targets preloaded must not carry the credential
|
||||
// through the association either.
|
||||
func TestWebhookMarshalsNoTargetConfig(t *testing.T) {
|
||||
func TestWebhookMarshalsNoTargetURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const marker = "QQNESTEDMARKERQQ"
|
||||
@@ -99,7 +110,7 @@ func TestWebhookMarshalsNoTargetConfig(t *testing.T) {
|
||||
Name: keptField,
|
||||
Targets: []database.Target{{
|
||||
Name: "slack",
|
||||
Config: `{"webhookUrl":"https://h/s/` + marker + `"}`,
|
||||
URL: "https://h/s/" + marker,
|
||||
MaxAttempts: 1,
|
||||
}},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package database
|
||||
|
||||
import "time"
|
||||
|
||||
// TargetType represents the type of delivery target
|
||||
type TargetType string
|
||||
|
||||
@@ -11,7 +13,9 @@ const (
|
||||
TargetTypeSlack TargetType = "slack"
|
||||
)
|
||||
|
||||
// Target represents a delivery target for a webhook
|
||||
// Target represents a delivery target for a webhook. Its settings
|
||||
// are the fields from URL to ArchiveRotation; each type uses some of
|
||||
// them, and the ones a type does not use stay empty.
|
||||
type Target struct {
|
||||
BaseModel
|
||||
|
||||
@@ -20,14 +24,35 @@ type Target struct {
|
||||
Type TargetType `gorm:"not null" json:"type"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
|
||||
// Configuration fields (JSON stored based on type).
|
||||
// URL is where an http or slack target sends each event.
|
||||
//
|
||||
// json:"-" because the blob holds the target's credential — a
|
||||
// Slack incoming-webhook URL, or an http destination whose path
|
||||
// segments are the secret. delivery.TargetView is the masking
|
||||
// barrier for the HTML path; this tag is the barrier for any
|
||||
// handler that marshals the model itself.
|
||||
Config string `gorm:"type:text" json:"-"` // JSON configuration
|
||||
// json:"-" because the URL is the target's credential: a Slack
|
||||
// incoming-webhook URL, or an http destination whose path segments
|
||||
// are the secret. delivery.TargetView is the masking barrier for
|
||||
// the HTML path; this tag is the barrier for any handler that
|
||||
// marshals the model itself.
|
||||
URL string `json:"-"`
|
||||
|
||||
// Headers is an http target's extra request headers, name to
|
||||
// value, stored as JSON text. json:"-" for the same reason as URL:
|
||||
// a header value is routinely an authorization token.
|
||||
Headers map[string]string `gorm:"serializer:json" json:"-"`
|
||||
|
||||
// TimeoutSeconds is an http target's per-request timeout; 0 uses
|
||||
// the delivery engine's shared client timeout.
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
|
||||
// ForwardQuery passes each event's query string on to an http
|
||||
// target, appended to URL. Off, URL is sent exactly as stored.
|
||||
ForwardQuery bool `json:"forwardQuery,omitempty"`
|
||||
|
||||
// ArchiveExpiry is how long a database target keeps each archived
|
||||
// event; 0 keeps them forever.
|
||||
ArchiveExpiry time.Duration `json:"archiveExpiry,omitempty"`
|
||||
|
||||
// ArchiveRotation is how often a database target starts a new
|
||||
// archive file: none, monthly, daily or hourly.
|
||||
ArchiveRotation string `json:"archiveRotation,omitempty"`
|
||||
|
||||
// MaxAttempts is how many times an HTTP or Slack target attempts
|
||||
// each delivery in all, at least 1. 1 is a single attempt with no
|
||||
|
||||
@@ -16,7 +16,7 @@ func newHTTPTarget(maxAttempts int) *database.Target {
|
||||
WebhookID: uuid.New().String(),
|
||||
Name: "target",
|
||||
Type: database.TargetTypeHTTP,
|
||||
Config: `{"url":"https://example.com/"}`,
|
||||
URL: "https://example.com/",
|
||||
MaxAttempts: maxAttempts,
|
||||
}
|
||||
}
|
||||
@@ -42,3 +42,22 @@ func TestTargetCreate_AcceptsOneAttempt(t *testing.T) {
|
||||
|
||||
require.NoError(t, db.Create(newHTTPTarget(1)).Error)
|
||||
}
|
||||
|
||||
// TestTargetHeaders_StoredAsJSON reads the headers column back as
|
||||
// text, to show it holds the header map as JSON.
|
||||
func TestTargetHeaders_StoredAsJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := startedTestDB(t)
|
||||
|
||||
target := newHTTPTarget(1)
|
||||
target.Headers = map[string]string{"Authorization": "Bearer x"}
|
||||
require.NoError(t, db.Create(target).Error)
|
||||
|
||||
var headers string
|
||||
|
||||
require.NoError(t, db.Raw(
|
||||
"SELECT headers FROM targets WHERE id = ?", target.ID,
|
||||
).Scan(&headers).Error)
|
||||
assert.JSONEq(t, `{"Authorization":"Bearer x"}`, headers)
|
||||
}
|
||||
|
||||
@@ -87,13 +87,18 @@ func TestMainSchemaHoldsEveryModel(t *testing.T) {
|
||||
Active: true,
|
||||
})
|
||||
assertRoundTrip(t, db, &database.Target{
|
||||
BaseModel: newBaseModel(),
|
||||
WebhookID: uuid.New().String(),
|
||||
Name: "target",
|
||||
Type: database.TargetTypeHTTP,
|
||||
Active: true,
|
||||
Config: `{"url":"https://example.com/"}`,
|
||||
MaxAttempts: 3,
|
||||
BaseModel: newBaseModel(),
|
||||
WebhookID: uuid.New().String(),
|
||||
Name: "target",
|
||||
Type: database.TargetTypeHTTP,
|
||||
Active: true,
|
||||
URL: "https://example.com/",
|
||||
Headers: map[string]string{"X-Api-Key": "key"},
|
||||
TimeoutSeconds: 30,
|
||||
ForwardQuery: true,
|
||||
ArchiveExpiry: 720 * time.Hour,
|
||||
ArchiveRotation: "daily",
|
||||
MaxAttempts: 3,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user