Files
webhooker/internal/database/schema_test.go
T
clawbot 40e8ecaab4
check / check (push) Waiting to run
Store a target's number of delivery attempts, at least one (closes #540)
Target.MaxRetries (max_retries) becomes Target.MaxAttempts
(max_attempts), and the form field, parser and ceiling are renamed to
match. The column is NOT NULL and a check constraint refuses a value
below 1, so every target, whatever its type, is stored with at least
1. 1 is a single attempt with no circuit breaker; above 1 retries
behind the breaker up to that many attempts. Both target forms refuse
0 and less; a blank field on the add target form gives 1, and on the
edit form keeps the stored value. The target page shows the plain
number, and the README's two paragraphs on 0 against 1 become one
sentence.

Model: opus-5-5
2026-10-15 19:38:14 +02:00

169 lines
4.6 KiB
Go

package database_test
import (
"log/slog"
"net/http"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"sneak.berlin/go/webhooker/internal/database"
"sneak.berlin/go/webhooker/internal/database/databasetest"
)
// withFixedNow returns db with GORM's clock stopped at a time in UTC,
// so that the created and updated times it writes come back from
// SQLite equal to the row written, which holds them in UTC.
func withFixedNow(db *gorm.DB) (*gorm.DB, time.Time) {
at := time.Date(2026, 10, 13, 12, 30, 45, 123456789, time.UTC)
return db.Session(&gorm.Session{
NowFunc: func() time.Time { return at },
}), at
}
// newBaseModel returns a BaseModel with a new ID.
func newBaseModel() database.BaseModel {
return database.BaseModel{ID: uuid.New().String()}
}
// assertRoundTrip writes row, which must be the only row of its
// table, and reads back the same row.
func assertRoundTrip[T any](t *testing.T, db *gorm.DB, row *T) {
t.Helper()
require.NoError(t, db.Create(row).Error)
var got T
require.NoError(t, db.First(&got).Error)
assert.Equal(t, *row, got)
}
// TestMainSchemaHoldsEveryModel writes a row of each main database
// model, every field set, into a database made from its SQL files.
func TestMainSchemaHoldsEveryModel(t *testing.T) {
t.Parallel()
mainDB, err := database.Open(
t.TempDir(), slog.New(slog.DiscardHandler),
)
require.NoError(t, err)
t.Cleanup(func() { _ = mainDB.Close() })
db, at := withFixedNow(mainDB.DB())
assertRoundTrip(t, db, &database.Setting{
Key: "session_key", Value: "value",
})
assertRoundTrip(t, db, &database.User{
BaseModel: newBaseModel(),
Username: "user",
Password: "password hash",
})
assertRoundTrip(t, db, &database.APIKey{
BaseModel: newBaseModel(),
UserID: uuid.New().String(),
Key: "key",
Description: "API key description",
LastUsedAt: &at,
})
assertRoundTrip(t, db, &database.Webhook{
BaseModel: newBaseModel(),
UserID: uuid.New().String(),
Name: "webhook",
Description: "webhook description",
RetentionDays: 7,
})
assertRoundTrip(t, db, &database.Entrypoint{
BaseModel: newBaseModel(),
WebhookID: uuid.New().String(),
Path: uuid.New().String(),
Description: "entrypoint description",
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,
})
}
// TestEventSchemaHoldsEveryModel does the same for a webhook's event
// database, which the manager makes from its SQL files.
func TestEventSchemaHoldsEveryModel(t *testing.T) {
t.Parallel()
mgr := databasetest.NewWebhookDBManager(t, t.TempDir())
t.Cleanup(func() { _ = mgr.CloseAll() })
events, err := mgr.GetDB(uuid.New().String())
require.NoError(t, err)
db, at := withFixedNow(events)
source := uuid.New().String()
assertRoundTrip(t, db, &database.Event{
BaseModel: newBaseModel(),
WebhookID: uuid.New().String(),
EntrypointID: uuid.New().String(),
Method: http.MethodPost,
RawQuery: "a=1",
Headers: `{"X":"Y"}`,
Body: "body",
ContentType: "text/plain",
BodyBytes: 4,
ResubmittedFromID: &source,
})
assertRoundTrip(t, db, &database.Delivery{
BaseModel: newBaseModel(),
EventID: uuid.New().String(),
TargetID: uuid.New().String(),
Status: database.DeliveryStatusDelivered,
FinishedAt: &at,
Replay: true,
})
assertRoundTrip(t, db, &database.DeliveryResult{
BaseModel: newBaseModel(),
DeliveryID: uuid.New().String(),
AttemptNum: 2,
Success: true,
StatusCode: 200,
ResponseBody: "ok",
Error: "error",
Duration: 15,
})
assertRoundTrip(t, db, &database.TargetTotals{
TargetID: uuid.New().String(),
Deliveries: 5,
Delivered: 3,
Failed: 1,
DeliveriesRemoved: 2,
FailedRemoved: 1,
})
assertRoundTrip(t, db, &database.EntrypointTotals{
EntrypointID: uuid.New().String(), LastEventAt: at,
})
// A new event database has its one row of event totals already.
require.NoError(t, database.AddEventTotals(db, database.EventTotals{
Events: 3, EventsRemoved: 1, LastEventAt: &at,
}))
var totals []database.EventTotals
require.NoError(t, db.Find(&totals).Error)
assert.Equal(t, []database.EventTotals{{
ID: 1, Events: 3, EventsRemoved: 1, LastEventAt: &at,
}}, totals)
}