check / check (push) Waiting to run
retention_days loses its column default of 30, and Webhook.RetentionDays its default tag, so a 0 is stored as 0. The 365000-day stand-in RetentionForeverDays and the BeforeSave hook that wrote it are gone. The reaper and the "forever" label read 0 as forever, and the forms accept 0 or 1 to MaxFiniteRetentionDays; a blank field still gives 30 on the new webhook form and keeps the stored value on the edit form. A negative value, which the forms refuse, is still read as forever so it cannot put the reaper's cutoff in the future. Model: opus-5-5
161 lines
3.8 KiB
Go
161 lines
3.8 KiB
Go
package database_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// startedTestDB returns a started main database for model-level tests.
|
|
func startedTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
|
|
db, lc := setupTestDB(t)
|
|
|
|
ctx := context.Background()
|
|
require.NoError(t, lc.Start(ctx))
|
|
t.Cleanup(func() { require.NoError(t, lc.Stop(ctx)) })
|
|
|
|
return db.DB()
|
|
}
|
|
|
|
// storedRetention reads the retention_days column straight out of the
|
|
// row, so the assertion is about what was persisted rather than about
|
|
// whatever the in-memory struct happens to hold.
|
|
func storedRetention(t *testing.T, db *gorm.DB, id string) int {
|
|
t.Helper()
|
|
|
|
var got int
|
|
|
|
require.NoError(
|
|
t,
|
|
db.Model(&database.Webhook{}).
|
|
Where("id = ?", id).
|
|
Pluck("retention_days", &got).Error,
|
|
)
|
|
|
|
return got
|
|
}
|
|
|
|
// newWebhookWithRetention creates a webhook through the ordinary Create
|
|
// path, as the new webhook form does.
|
|
func newWebhookWithRetention(
|
|
t *testing.T,
|
|
db *gorm.DB,
|
|
wh *database.Webhook,
|
|
) string {
|
|
t.Helper()
|
|
|
|
wh.UserID = uuid.New().String()
|
|
wh.Name = testWebhookName
|
|
|
|
require.NoError(
|
|
t,
|
|
db.Omit(clause.Associations).Create(wh).Error,
|
|
)
|
|
|
|
return wh.ID
|
|
}
|
|
|
|
func TestWebhookRetentionDays_ZeroIsStoredAsZero(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := startedTestDB(t)
|
|
|
|
wh := &database.Webhook{RetentionDays: 0}
|
|
id := newWebhookWithRetention(t, db, wh)
|
|
|
|
assert.Equal(t, 0, storedRetention(t, db, id))
|
|
}
|
|
|
|
func TestWebhookRetentionDays_DayCountIsStored(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := startedTestDB(t)
|
|
|
|
wh := &database.Webhook{RetentionDays: 7}
|
|
id := newWebhookWithRetention(t, db, wh)
|
|
|
|
assert.Equal(t, 7, storedRetention(t, db, id))
|
|
}
|
|
|
|
// TestWebhookRetentionDays_UpdateToZeroIsStoredAsZero changes a day
|
|
// count to 0 through the same Save call the edit handler makes.
|
|
func TestWebhookRetentionDays_UpdateToZeroIsStoredAsZero(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := startedTestDB(t)
|
|
|
|
wh := &database.Webhook{RetentionDays: 30}
|
|
id := newWebhookWithRetention(t, db, wh)
|
|
require.Equal(t, 30, storedRetention(t, db, id))
|
|
|
|
wh.RetentionDays = 0
|
|
require.NoError(t, db.Omit(clause.Associations).Save(wh).Error)
|
|
|
|
assert.Equal(t, 0, storedRetention(t, db, id))
|
|
}
|
|
|
|
// TestMaxFiniteRetentionDaysIsTheOverflowCeiling asserts that the
|
|
// constant is exactly where the cutoff arithmetic stops working, which
|
|
// is what makes it a derived bound rather than a round number someone
|
|
// liked. One day more wraps the int64 nanosecond count negative, and a
|
|
// negative span is precisely what turned a cutoff into a future
|
|
// timestamp that matched — and deleted — every row.
|
|
//
|
|
// The multiplications are done through variables on purpose: as
|
|
// constant expressions the overflowing one would not compile.
|
|
func TestMaxFiniteRetentionDaysIsTheOverflowCeiling(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const hoursPerDay = 24
|
|
|
|
atCeiling := database.MaxFiniteRetentionDays
|
|
overCeiling := database.MaxFiniteRetentionDays + 1
|
|
|
|
assert.Positive(
|
|
t,
|
|
time.Duration(atCeiling*hoursPerDay)*time.Hour,
|
|
"the ceiling itself must still be representable",
|
|
)
|
|
assert.Negative(
|
|
t,
|
|
time.Duration(overCeiling*hoursPerDay)*time.Hour,
|
|
"one day past the ceiling must overflow",
|
|
)
|
|
}
|
|
|
|
func TestWebhookRetainsForeverAndLabel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
days int
|
|
forever bool
|
|
label string
|
|
}{
|
|
{"zero", 0, true, testForeverLabel},
|
|
{"negative", -1, true, testForeverLabel},
|
|
{"default", database.DefaultRetentionDays, false, "30 days"},
|
|
{"one day", 1, false, "1 day"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wh := database.Webhook{RetentionDays: tc.days}
|
|
|
|
assert.Equal(t, tc.forever, wh.RetainsForever())
|
|
assert.Equal(t, tc.label, wh.RetentionLabel())
|
|
})
|
|
}
|
|
}
|