check / check (push) Waiting to run
Target.MaxRetries (max_retries) becomes Target.MaxAttempts (max_attempts), and the form field, parser and ceiling are renamed to match. 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
169 lines
4.6 KiB
Go
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)
|
|
}
|