check / check (push) Waiting to run
The main database, each webhook's event database and each archive file get their schema from SQL files under internal/db/migrations/, one directory per kind. Opening a file runs, in one transaction, each file that schema_migrations does not list. GORM stays for queries. AutoMigrate is gone, tests included. An event database holds only its own tables now, so the sweep that removed target rows copied into it is deleted. The association guard stays: without it a delivery write fails on the missing targets table. The event totals row is inserted by events/001_schema.sql rather than on every open. Model: opus-5-5
166 lines
4.5 KiB
Go
166 lines
4.5 KiB
Go
package database_test
|
|
|
|
import (
|
|
"log/slog"
|
|
"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: "hash",
|
|
})
|
|
assertRoundTrip(t, db, &database.APIKey{
|
|
BaseModel: newBaseModel(),
|
|
UserID: uuid.New().String(),
|
|
Key: "key",
|
|
Description: "description",
|
|
LastUsedAt: &at,
|
|
})
|
|
assertRoundTrip(t, db, &database.Webhook{
|
|
BaseModel: newBaseModel(),
|
|
UserID: uuid.New().String(),
|
|
Name: "webhook",
|
|
Description: "description",
|
|
RetentionDays: 7,
|
|
})
|
|
assertRoundTrip(t, db, &database.Entrypoint{
|
|
BaseModel: newBaseModel(),
|
|
WebhookID: uuid.New().String(),
|
|
Path: uuid.New().String(),
|
|
Description: "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/"}`,
|
|
MaxRetries: 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: "POST",
|
|
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)
|
|
}
|