package processing import ( "context" "os" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) func TestCleanupExpiredSyncJobsKeepsLatestHashAndDeletesAged(t *testing.T) { dsn := os.Getenv("DATABASE_URL") if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatal(err) } defer pg.Close() companyID := uuid.New() _, err = pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`, companyID, "retention-"+companyID.String()[:8]) if err != nil { t.Fatalf("seed company: %v", err) } t.Cleanup(func() { _, _ = pg.Exec(context.Background(), `DELETE FROM companies WHERE id = $1`, companyID) }) insertFeed := func(name string) uuid.UUID { t.Helper() var id uuid.UUID err := pg.QueryRow(ctx, ` INSERT INTO input_feeds (company_id, name, url, feed_type, status, sync_interval_minutes, options) VALUES ($1, $2, '', 'csv', 'active', 60, '{}'::jsonb) RETURNING id`, companyID, name).Scan(&id) if err != nil { t.Fatal(err) } return id } insertSync := func(feedID uuid.UUID, status, age, hash string) uuid.UUID { t.Helper() var id uuid.UUID q := ` INSERT INTO feed_sync_jobs ( feed_id, company_id, status, started_at, completed_at, updated_at, created_at, content_hash ) VALUES ( $1, $2, $3, now() - interval '` + age + `', now() - interval '` + age + `', now() - interval '` + age + `', now() - interval '` + age + `', NULLIF($4, '') ) RETURNING id` if err := pg.QueryRow(ctx, q, feedID, companyID, status, hash).Scan(&id); err != nil { t.Fatal(err) } return id } assertGone := func(id uuid.UUID, label string) { t.Helper() var n int if err := pg.QueryRow(ctx, `SELECT COUNT(*) FROM feed_sync_jobs WHERE id = $1`, id).Scan(&n); err != nil { t.Fatal(err) } if n != 0 { t.Fatalf("%s still present", label) } } assertPresent := func(id uuid.UUID, label string) { t.Helper() var n int if err := pg.QueryRow(ctx, `SELECT COUNT(*) FROM feed_sync_jobs WHERE id = $1`, id).Scan(&n); err != nil { t.Fatal(err) } if n != 1 { t.Fatalf("%s missing", label) } } t.Run("deletesAgedWhenFresherHashExists", func(t *testing.T) { feedID := insertFeed("retention-fresh") olderHash := insertSync(feedID, "completed", "60 days", "hash-old") midHash := insertSync(feedID, "completed", "45 days", "hash-mid") oldFailed := insertSync(feedID, "failed", "45 days", "") freshCompleted := insertSync(feedID, "completed", "1 day", "hash-fresh") oldRunning := insertSync(feedID, "running", "45 days", "") res, err := CleanupExpiredSyncJobs(ctx, pg) if err != nil { t.Fatal(err) } if res.SyncJobsDeleted < 3 { t.Fatalf("sync_jobs_deleted=%d want >= 3", res.SyncJobsDeleted) } assertGone(olderHash, "older completed hash") assertGone(midHash, "mid completed hash") assertGone(oldFailed, "old failed") assertPresent(freshCompleted, "fresh completed") assertPresent(oldRunning, "old running") }) t.Run("keepsNewestAgedHashWhenNoFresher", func(t *testing.T) { feedID := insertFeed("retention-stale") olderHash := insertSync(feedID, "completed", "60 days", "hash-old") keepHash := insertSync(feedID, "completed", "45 days", "hash-keep") oldFailed := insertSync(feedID, "failed", "45 days", "") res, err := CleanupExpiredSyncJobs(ctx, pg) if err != nil { t.Fatal(err) } if res.SyncJobsDeleted < 2 { t.Fatalf("sync_jobs_deleted=%d want >= 2", res.SyncJobsDeleted) } assertGone(olderHash, "older completed hash") assertGone(oldFailed, "old failed") assertPresent(keepHash, "newest aged completed with hash") var hash string err = pg.QueryRow(ctx, ` SELECT content_hash FROM feed_sync_jobs WHERE feed_id = $1 AND status = 'completed' AND content_hash IS NOT NULL AND content_hash <> '' ORDER BY completed_at DESC NULLS LAST LIMIT 1`, feedID).Scan(&hash) if err != nil { t.Fatal(err) } if hash != "hash-keep" { t.Fatalf("lastContentHash=%q want hash-keep", hash) } }) }