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()) }) } }