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 and default a blank field to 1. 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)
|
|
}
|