Add per-webhook event retention reaper (closes #63)
All checks were successful
check / check (push) Successful in 5s
All checks were successful
check / check (push) Successful in 5s
This commit is contained in:
277
internal/database/retention_test.go
Normal file
277
internal/database/retention_test.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/fx/fxtest"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"sneak.berlin/go/webhooker/internal/config"
|
||||
"sneak.berlin/go/webhooker/internal/database"
|
||||
"sneak.berlin/go/webhooker/internal/globals"
|
||||
"sneak.berlin/go/webhooker/internal/logger"
|
||||
)
|
||||
|
||||
// retentionTestEnv bundles the pieces a retention test drives.
|
||||
type retentionTestEnv struct {
|
||||
reaper *database.RetentionReaper
|
||||
mainDB *database.Database
|
||||
mgr *database.WebhookDBManager
|
||||
}
|
||||
|
||||
func setupRetentionTest(t *testing.T) *retentionTestEnv {
|
||||
t.Helper()
|
||||
|
||||
lc := fxtest.NewLifecycle(t)
|
||||
|
||||
g := &globals.Globals{
|
||||
Appname: "webhooker-test",
|
||||
Version: "test",
|
||||
}
|
||||
|
||||
l, err := logger.New(lc, logger.LoggerParams{Globals: g})
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &config.Config{
|
||||
DataDir: t.TempDir(),
|
||||
Environment: "dev",
|
||||
}
|
||||
|
||||
mainDB, err := database.New(lc, database.DatabaseParams{
|
||||
Config: cfg,
|
||||
Logger: l,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
mgr, err := database.NewWebhookDBManager(
|
||||
lc,
|
||||
database.WebhookDBManagerParams{Config: cfg, Logger: l},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
require.NoError(t, lc.Start(ctx))
|
||||
t.Cleanup(func() { require.NoError(t, lc.Stop(ctx)) })
|
||||
|
||||
return &retentionTestEnv{
|
||||
reaper: database.NewTestRetentionReaper(mainDB, mgr),
|
||||
mainDB: mainDB,
|
||||
mgr: mgr,
|
||||
}
|
||||
}
|
||||
|
||||
// createWebhook inserts a webhook row into the main database with the
|
||||
// given retention policy and returns its ID.
|
||||
func createWebhook(
|
||||
t *testing.T,
|
||||
db *gorm.DB,
|
||||
retentionDays int,
|
||||
) string {
|
||||
t.Helper()
|
||||
|
||||
wh := &database.Webhook{
|
||||
UserID: uuid.New().String(),
|
||||
Name: "test-webhook",
|
||||
RetentionDays: retentionDays,
|
||||
}
|
||||
require.NoError(
|
||||
t,
|
||||
db.Omit(clause.Associations).Create(wh).Error,
|
||||
)
|
||||
|
||||
// The RetentionDays column carries a GORM default of 30, so a
|
||||
// zero (or negative) value passed to Create is replaced by that
|
||||
// default. Force the requested value explicitly so the
|
||||
// retain-forever (<= 0) path can be exercised.
|
||||
require.NoError(
|
||||
t,
|
||||
db.Model(wh).
|
||||
Update("retention_days", retentionDays).Error,
|
||||
)
|
||||
|
||||
return wh.ID
|
||||
}
|
||||
|
||||
// eventChain is the set of row IDs seeded for a single event.
|
||||
type eventChain struct {
|
||||
eventID string
|
||||
deliveryID string
|
||||
resultID string
|
||||
}
|
||||
|
||||
// seedEventChain creates an event with one delivery and one delivery
|
||||
// result, all stamped with createdAt, and returns their IDs.
|
||||
func seedEventChain(
|
||||
t *testing.T,
|
||||
db *gorm.DB,
|
||||
webhookID string,
|
||||
createdAt time.Time,
|
||||
) eventChain {
|
||||
t.Helper()
|
||||
|
||||
event := &database.Event{
|
||||
WebhookID: webhookID,
|
||||
EntrypointID: uuid.New().String(),
|
||||
Method: "POST",
|
||||
Body: `{"seed": true}`,
|
||||
ContentType: "application/json",
|
||||
}
|
||||
event.CreatedAt = createdAt
|
||||
require.NoError(t, db.Create(event).Error)
|
||||
|
||||
delivery := &database.Delivery{
|
||||
EventID: event.ID,
|
||||
TargetID: uuid.New().String(),
|
||||
Status: database.DeliveryStatusDelivered,
|
||||
}
|
||||
delivery.CreatedAt = createdAt
|
||||
require.NoError(t, db.Create(delivery).Error)
|
||||
|
||||
result := &database.DeliveryResult{
|
||||
DeliveryID: delivery.ID,
|
||||
AttemptNum: 1,
|
||||
Success: true,
|
||||
StatusCode: 200,
|
||||
Duration: 10,
|
||||
}
|
||||
result.CreatedAt = createdAt
|
||||
require.NoError(t, db.Create(result).Error)
|
||||
|
||||
return eventChain{
|
||||
eventID: event.ID,
|
||||
deliveryID: delivery.ID,
|
||||
resultID: result.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// countByID returns how many rows of model match the given id,
|
||||
// counting even hard-deletable rows via Unscoped.
|
||||
func countByID(
|
||||
t *testing.T,
|
||||
db *gorm.DB,
|
||||
model any,
|
||||
id string,
|
||||
) int64 {
|
||||
t.Helper()
|
||||
|
||||
var n int64
|
||||
|
||||
require.NoError(
|
||||
t,
|
||||
db.Unscoped().Model(model).
|
||||
Where("id = ?", id).Count(&n).Error,
|
||||
)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func assertChainGone(
|
||||
t *testing.T,
|
||||
db *gorm.DB,
|
||||
chain eventChain,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
assert.Zero(
|
||||
t,
|
||||
countByID(t, db, &database.Event{}, chain.eventID),
|
||||
"expired event should be removed",
|
||||
)
|
||||
assert.Zero(
|
||||
t,
|
||||
countByID(t, db, &database.Delivery{}, chain.deliveryID),
|
||||
"expired delivery should be removed",
|
||||
)
|
||||
assert.Zero(
|
||||
t,
|
||||
countByID(
|
||||
t, db, &database.DeliveryResult{}, chain.resultID,
|
||||
),
|
||||
"expired delivery result should be removed",
|
||||
)
|
||||
}
|
||||
|
||||
func assertChainPresent(
|
||||
t *testing.T,
|
||||
db *gorm.DB,
|
||||
chain eventChain,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(
|
||||
t,
|
||||
int64(1),
|
||||
countByID(t, db, &database.Event{}, chain.eventID),
|
||||
"recent event should be retained",
|
||||
)
|
||||
assert.Equal(
|
||||
t,
|
||||
int64(1),
|
||||
countByID(t, db, &database.Delivery{}, chain.deliveryID),
|
||||
"recent delivery should be retained",
|
||||
)
|
||||
assert.Equal(
|
||||
t,
|
||||
int64(1),
|
||||
countByID(
|
||||
t, db, &database.DeliveryResult{}, chain.resultID,
|
||||
),
|
||||
"recent delivery result should be retained",
|
||||
)
|
||||
}
|
||||
|
||||
func TestRetentionReaper_ReapsExpiredKeepsRecent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupRetentionTest(t)
|
||||
|
||||
const retentionDays = 30
|
||||
|
||||
webhookID := createWebhook(
|
||||
t, env.mainDB.DB(), retentionDays,
|
||||
)
|
||||
|
||||
db, err := env.mgr.GetDB(webhookID)
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
old := seedEventChain(
|
||||
t, db, webhookID,
|
||||
now.Add(-40*24*time.Hour),
|
||||
)
|
||||
recent := seedEventChain(
|
||||
t, db, webhookID,
|
||||
now.Add(-1*24*time.Hour),
|
||||
)
|
||||
|
||||
env.reaper.ExportSweep(context.Background())
|
||||
|
||||
assertChainGone(t, db, old)
|
||||
assertChainPresent(t, db, recent)
|
||||
}
|
||||
|
||||
func TestRetentionReaper_RetainsForeverWhenNonPositive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupRetentionTest(t)
|
||||
|
||||
// RetentionDays of zero means retain forever.
|
||||
webhookID := createWebhook(t, env.mainDB.DB(), 0)
|
||||
|
||||
db, err := env.mgr.GetDB(webhookID)
|
||||
require.NoError(t, err)
|
||||
|
||||
ancient := seedEventChain(
|
||||
t, db, webhookID,
|
||||
time.Now().Add(-365*24*time.Hour),
|
||||
)
|
||||
|
||||
env.reaper.ExportSweep(context.Background())
|
||||
|
||||
assertChainPresent(t, db, ancient)
|
||||
}
|
||||
Reference in New Issue
Block a user