Expand Up @@ -1271,195 +1271,138 @@ func TestDeleteOldAuditLogs(t *testing.T) { func TestDeleteExpiredAPIKeys(t *testing.T) { t.Parallel() t.Run("RetentionEnabled", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) clk := quartz.NewMock(t) now := time.Date(2025, 1, 15, 7, 30, 0, 0, time.UTC) retentionPeriod := 7 * 24 * time.Hour // 7 days expiredLongAgo := now.Add(-retentionPeriod).Add(-24 * time.Hour) // Expired 8 days ago (should be deleted) expiredRecently := now.Add(-retentionPeriod).Add(24 * time.Hour) // Expired 6 days ago (should be kept) notExpired := now.Add(24 * time.Hour) // Expires tomorrow (should be kept) clk.Set(now).MustWait(ctx) db, _ := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) user := dbgen.User(t, db, database.User{}) // Create API key that expired long ago (should be deleted) oldExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: expiredLongAgo, TokenName: "old-expired-key", }) // Create API key that expired recently (should be kept) recentExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: expiredRecently, TokenName: "recent-expired-key", }) // Create API key that hasn't expired yet (should be kept) activeKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: notExpired, TokenName: "active-key", }) now := time.Date(2025, 1, 15, 7, 30, 0, 0, time.UTC) // Run the purge with configured retention period done := awaitDoTick(ctx, t, clk) closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{ Retention: codersdk.RetentionConfig{ APIKeys: serpent.Duration(retentionPeriod), testCases := []struct { name string retentionConfig codersdk.RetentionConfig oldExpiredTime time.Time recentExpiredTime *time.Time // nil means no recent expired key created activeTime *time.Time // nil means no active key created expectOldExpiredDeleted bool expectedKeysRemaining int }{ { name: "RetentionEnabled", retentionConfig: codersdk.RetentionConfig{ APIKeys: serpent.Duration(7 * 24 * time.Hour), // 7 days }, }, clk) defer closer.Close() testutil.TryReceive(ctx, t, done) // Verify results _, err := db.GetAPIKeyByID(ctx, oldExpiredKey.ID) require.Error(t, err, "old expired key should be deleted") _, err = db.GetAPIKeyByID(ctx, recentExpiredKey.ID) require.NoError(t, err, "recently expired key should be kept") _, err = db.GetAPIKeyByID(ctx, activeKey.ID) require.NoError(t, err, "active key should be kept") }) t.Run("RetentionDisabled", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) clk := quartz.NewMock(t) now := time.Date(2025, 1, 15, 7, 30, 0, 0, time.UTC) expiredLongAgo := now.Add(-365 * 24 * time.Hour) // Expired 1 year ago clk.Set(now).MustWait(ctx) db, _ := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) user := dbgen.User(t, db, database.User{}) // Create API key that expired long ago (should NOT be deleted when retention is 0) oldExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: expiredLongAgo, TokenName: "old-expired-key", }) // Run the purge with retention disabled (0) done := awaitDoTick(ctx, t, clk) closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{ Retention: codersdk.RetentionConfig{ APIKeys: serpent.Duration(0), // disabled oldExpiredTime: now.Add(-8 * 24 * time.Hour), // Expired 8 days ago recentExpiredTime: ptr(now.Add(-6 * 24 * time.Hour)), // Expired 6 days ago activeTime: ptr(now.Add(24 * time.Hour)), // Expires tomorrow expectOldExpiredDeleted: true, expectedKeysRemaining: 2, // recent expired + active }, { name: "RetentionDisabled", retentionConfig: codersdk.RetentionConfig{ APIKeys: serpent.Duration(0), }, }, clk) defer closer.Close() testutil.TryReceive(ctx, t, done) // Verify old expired key is still present _, err := db.GetAPIKeyByID(ctx, oldExpiredKey.ID) require.NoError(t, err, "old expired key should NOT be deleted when retention is disabled") }) t.Run("GlobalRetentionFallback", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) clk := quartz.NewMock(t) now := time.Date(2025, 1, 15, 7, 30, 0, 0, time.UTC) retentionPeriod := 14 * 24 * time.Hour // 14 days global expiredLongAgo := now.Add(-retentionPeriod).Add(-24 * time.Hour) // Expired 15 days ago (should be deleted) expiredRecently := now.Add(-retentionPeriod).Add(24 * time.Hour) // Expired 13 days ago (should be kept) clk.Set(now).MustWait(ctx) db, _ := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) user := dbgen.User(t, db, database.User{}) oldExpiredTime: now.Add(-365 * 24 * time.Hour), // Expired 1 year ago recentExpiredTime: nil, activeTime: nil, expectOldExpiredDeleted: false, expectedKeysRemaining: 1, // old expired is kept }, { name: "GlobalRetentionFallback", retentionConfig: codersdk.RetentionConfig{ Global: serpent.Duration(14 * 24 * time.Hour), // 14 days global APIKeys: serpent.Duration(0), // Not set, should fall back to global }, oldExpiredTime: now.Add(-15 * 24 * time.Hour), // Expired 15 days ago recentExpiredTime: ptr(now.Add(-13 * 24 * time.Hour)), // Expired 13 days ago activeTime: nil, expectOldExpiredDeleted: true, expectedKeysRemaining: 1, // only recent expired remains }, { name: "CustomRetention30Days", retentionConfig: codersdk.RetentionConfig{ APIKeys: serpent.Duration(30 * 24 * time.Hour), // 30 days }, oldExpiredTime: now.Add(-31 * 24 * time.Hour), // Expired 31 days ago recentExpiredTime: ptr(now.Add(-29 * 24 * time.Hour)), // Expired 29 days ago activeTime: nil, expectOldExpiredDeleted: true, expectedKeysRemaining: 1, // only recent expired remains }, } // Create API key that expired long ago (should be deleted) oldExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: expiredLongAgo, TokenName: "old-expired-key", }) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // Create API key that expired recently (should be kept) recentExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: expiredRecently, TokenName: "recent-expired-key", }) ctx := testutil.Context(t, testutil.WaitShort) clk := quartz.NewMock(t) clk.Set(now).MustWait(ctx) // Run the purge with global retention (API keys retention is 0, so it falls back) done := awaitDoTick(ctx, t, clk) closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{ Retention: codersdk.RetentionConfig{ Global: serpent.Duration(retentionPeriod), // Use global APIKeys: serpent.Duration(0), // Not set, should fall back to global }, }, clk) defer closer.Close() testutil.TryReceive(ctx, t, done) db, _ := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) user := dbgen.User(t, db, database.User{}) // Verify results _, err := db.GetAPIKeyByID(ctx, oldExpiredKey.ID) require.Error(t, err, "old expired key should be deleted via global retention") // Create API key that expired long ago. oldExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: tc.oldExpiredTime, TokenName: "old-expired-key", }) _, err = db.GetAPIKeyByID(ctx, recentExpiredKey.ID) require.NoError(t, err, "recently expired key should be kept") }) // Create API key that expired recently if specified. var recentExpiredKey database.APIKey if tc.recentExpiredTime != nil { recentExpiredKey, _ = dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: *tc.recentExpiredTime, TokenName: "recent-expired-key", }) } t.Run("CustomRetention30Days", func(t *testing.T) { t.Parallel() // Create API key that hasn't expired yet if specified. var activeKey database.APIKey if tc.activeTime != nil { activeKey, _ = dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: *tc.activeTime, TokenName: "active-key", }) } ctx := testutil.Context(t, testutil.WaitShort) // Run the purge. done := awaitDoTick(ctx, t, clk) closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{ Retention: tc.retentionConfig, }, clk) defer closer.Close() testutil.TryReceive(ctx, t, done) clk := quartz.NewMock(t) now := time.Date(2025, 1, 15, 7, 30, 0, 0, time.UTC) retentionPeriod := 30 * 24 * time.Hour // 30 days expiredLongAgo := now.Add(-retentionPeriod).Add(-24 * time.Hour) // Expired 31 days ago (should be deleted) expiredRecently := now.Add(-retentionPeriod).Add(24 * time.Hour) // Expired 29 days ago (should be kept) clk.Set(now).MustWait(ctx) // Verify total keys remaining. keys, err := db.GetAPIKeysLastUsedAfter(ctx, time.Time{}) require.NoError(t, err) require.Len(t, keys, tc.expectedKeysRemaining, "unexpected number of keys remaining") db, _ := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) user := dbgen.User(t, db, database.User{}) // Verify results. _, err = db.GetAPIKeyByID(ctx, oldExpiredKey.ID) if tc.expectOldExpiredDeleted { require.Error(t, err, "old expired key should be deleted") } else { require.NoError(t, err, "old expired key should NOT be deleted") } // Create API key that expired long ago (should be deleted) oldExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: expiredLongAgo, TokenName: "old-expired-key", }) if tc.recentExpiredTime != nil { _, err = db.GetAPIKeyByID(ctx, recentExpiredKey.ID) require.NoError(t, err, "recently expired key should be kept") } // Create API key that expired recently (should be kept) recentExpiredKey, _ := dbgen.APIKey(t, db, database.APIKey{ UserID: user.ID, ExpiresAt: expiredRecently, TokenName: "recent-expired-key", if tc.activeTime != nil { _, err = db.GetAPIKeyByID(ctx, activeKey.ID) require.NoError(t, err, "active key should be kept") } }) } } // Run the purge with 30-day retention done := awaitDoTick(ctx, t, clk) closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{ Retention: codersdk.RetentionConfig{ APIKeys: serpent.Duration(retentionPeriod), }, }, clk) defer closer.Close() testutil.TryReceive(ctx, t, done) // Verify results _, err := db.GetAPIKeyByID(ctx, oldExpiredKey.ID) require.Error(t, err, "old expired key should be deleted with 30-day retention") _, err = db.GetAPIKeyByID(ctx, recentExpiredKey.ID) require.NoError(t, err, "recently expired key should be kept with 30-day retention") }) // ptr is a helper to create a pointer to a value. func ptr[T any](v T) *T { return &v }