@@ -1249,195 +1249,138 @@ func TestDeleteOldAuditLogs(t *testing.T) {
12491249func TestDeleteExpiredAPIKeys (t * testing.T ) {
12501250 t .Parallel ()
12511251
1252- t .Run ("RetentionEnabled" , func (t * testing.T ) {
1253- t .Parallel ()
1254-
1255- ctx := testutil .Context (t , testutil .WaitShort )
1256-
1257- clk := quartz .NewMock (t )
1258- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1259- retentionPeriod := 7 * 24 * time .Hour // 7 days
1260- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // Expired 8 days ago (should be deleted)
1261- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour ) // Expired 6 days ago (should be kept)
1262- notExpired := now .Add (24 * time .Hour ) // Expires tomorrow (should be kept)
1263- clk .Set (now ).MustWait (ctx )
1264-
1265- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1266- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1267- user := dbgen .User (t , db , database.User {})
1268-
1269- // Create API key that expired long ago (should be deleted)
1270- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1271- UserID : user .ID ,
1272- ExpiresAt : expiredLongAgo ,
1273- TokenName : "old-expired-key" ,
1274- })
1275-
1276- // Create API key that expired recently (should be kept)
1277- recentExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1278- UserID : user .ID ,
1279- ExpiresAt : expiredRecently ,
1280- TokenName : "recent-expired-key" ,
1281- })
1282-
1283- // Create API key that hasn't expired yet (should be kept)
1284- activeKey , _ := dbgen .APIKey (t , db , database.APIKey {
1285- UserID : user .ID ,
1286- ExpiresAt : notExpired ,
1287- TokenName : "active-key" ,
1288- })
1252+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
12891253
1290- // Run the purge with configured retention period
1291- done := awaitDoTick (ctx , t , clk )
1292- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1293- Retention : codersdk.RetentionConfig {
1294- APIKeys : serpent .Duration (retentionPeriod ),
1254+ testCases := []struct {
1255+ name string
1256+ retentionConfig codersdk.RetentionConfig
1257+ oldExpiredTime time.Time
1258+ recentExpiredTime * time.Time // nil means no recent expired key created
1259+ activeTime * time.Time // nil means no active key created
1260+ expectOldExpiredDeleted bool
1261+ expectedKeysRemaining int
1262+ }{
1263+ {
1264+ name : "RetentionEnabled" ,
1265+ retentionConfig : codersdk.RetentionConfig {
1266+ APIKeys : serpent .Duration (7 * 24 * time .Hour ), // 7 days
12951267 },
1296- }, clk )
1297- defer closer .Close ()
1298- testutil .TryReceive (ctx , t , done )
1299-
1300- // Verify results
1301- _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1302- require .Error (t , err , "old expired key should be deleted" )
1303-
1304- _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1305- require .NoError (t , err , "recently expired key should be kept" )
1306-
1307- _ , err = db .GetAPIKeyByID (ctx , activeKey .ID )
1308- require .NoError (t , err , "active key should be kept" )
1309- })
1310-
1311- t .Run ("RetentionDisabled" , func (t * testing.T ) {
1312- t .Parallel ()
1313-
1314- ctx := testutil .Context (t , testutil .WaitShort )
1315-
1316- clk := quartz .NewMock (t )
1317- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1318- expiredLongAgo := now .Add (- 365 * 24 * time .Hour ) // Expired 1 year ago
1319- clk .Set (now ).MustWait (ctx )
1320-
1321- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1322- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1323- user := dbgen .User (t , db , database.User {})
1324-
1325- // Create API key that expired long ago (should NOT be deleted when retention is 0)
1326- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1327- UserID : user .ID ,
1328- ExpiresAt : expiredLongAgo ,
1329- TokenName : "old-expired-key" ,
1330- })
1331-
1332- // Run the purge with retention disabled (0)
1333- done := awaitDoTick (ctx , t , clk )
1334- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1335- Retention : codersdk.RetentionConfig {
1336- APIKeys : serpent .Duration (0 ), // disabled
1268+ oldExpiredTime : now .Add (- 8 * 24 * time .Hour ), // Expired 8 days ago
1269+ recentExpiredTime : ptr (now .Add (- 6 * 24 * time .Hour )), // Expired 6 days ago
1270+ activeTime : ptr (now .Add (24 * time .Hour )), // Expires tomorrow
1271+ expectOldExpiredDeleted : true ,
1272+ expectedKeysRemaining : 2 , // recent expired + active
1273+ },
1274+ {
1275+ name : "RetentionDisabled" ,
1276+ retentionConfig : codersdk.RetentionConfig {
1277+ APIKeys : serpent .Duration (0 ),
13371278 },
1338- }, clk )
1339- defer closer .Close ()
1340- testutil .TryReceive (ctx , t , done )
1341-
1342- // Verify old expired key is still present
1343- _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1344- require .NoError (t , err , "old expired key should NOT be deleted when retention is disabled" )
1345- })
1346-
1347- t .Run ("GlobalRetentionFallback" , func (t * testing.T ) {
1348- t .Parallel ()
1349-
1350- ctx := testutil .Context (t , testutil .WaitShort )
1351-
1352- clk := quartz .NewMock (t )
1353- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1354- retentionPeriod := 14 * 24 * time .Hour // 14 days global
1355- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // Expired 15 days ago (should be deleted)
1356- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour ) // Expired 13 days ago (should be kept)
1357- clk .Set (now ).MustWait (ctx )
1358-
1359- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1360- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1361- user := dbgen .User (t , db , database.User {})
1279+ oldExpiredTime : now .Add (- 365 * 24 * time .Hour ), // Expired 1 year ago
1280+ recentExpiredTime : nil ,
1281+ activeTime : nil ,
1282+ expectOldExpiredDeleted : false ,
1283+ expectedKeysRemaining : 1 , // old expired is kept
1284+ },
1285+ {
1286+ name : "GlobalRetentionFallback" ,
1287+ retentionConfig : codersdk.RetentionConfig {
1288+ Global : serpent .Duration (14 * 24 * time .Hour ), // 14 days global
1289+ APIKeys : serpent .Duration (0 ), // Not set, should fall back to global
1290+ },
1291+ oldExpiredTime : now .Add (- 15 * 24 * time .Hour ), // Expired 15 days ago
1292+ recentExpiredTime : ptr (now .Add (- 13 * 24 * time .Hour )), // Expired 13 days ago
1293+ activeTime : nil ,
1294+ expectOldExpiredDeleted : true ,
1295+ expectedKeysRemaining : 1 , // only recent expired remains
1296+ },
1297+ {
1298+ name : "CustomRetention30Days" ,
1299+ retentionConfig : codersdk.RetentionConfig {
1300+ APIKeys : serpent .Duration (30 * 24 * time .Hour ), // 30 days
1301+ },
1302+ oldExpiredTime : now .Add (- 31 * 24 * time .Hour ), // Expired 31 days ago
1303+ recentExpiredTime : ptr (now .Add (- 29 * 24 * time .Hour )), // Expired 29 days ago
1304+ activeTime : nil ,
1305+ expectOldExpiredDeleted : true ,
1306+ expectedKeysRemaining : 1 , // only recent expired remains
1307+ },
1308+ }
13621309
1363- // Create API key that expired long ago (should be deleted)
1364- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1365- UserID : user .ID ,
1366- ExpiresAt : expiredLongAgo ,
1367- TokenName : "old-expired-key" ,
1368- })
1310+ for _ , tc := range testCases {
1311+ t .Run (tc .name , func (t * testing.T ) {
1312+ t .Parallel ()
13691313
1370- // Create API key that expired recently (should be kept)
1371- recentExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1372- UserID : user .ID ,
1373- ExpiresAt : expiredRecently ,
1374- TokenName : "recent-expired-key" ,
1375- })
1314+ ctx := testutil .Context (t , testutil .WaitShort )
1315+ clk := quartz .NewMock (t )
1316+ clk .Set (now ).MustWait (ctx )
13761317
1377- // Run the purge with global retention (API keys retention is 0, so it falls back)
1378- done := awaitDoTick (ctx , t , clk )
1379- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1380- Retention : codersdk.RetentionConfig {
1381- Global : serpent .Duration (retentionPeriod ), // Use global
1382- APIKeys : serpent .Duration (0 ), // Not set, should fall back to global
1383- },
1384- }, clk )
1385- defer closer .Close ()
1386- testutil .TryReceive (ctx , t , done )
1318+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1319+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1320+ user := dbgen .User (t , db , database.User {})
13871321
1388- // Verify results
1389- _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1390- require .Error (t , err , "old expired key should be deleted via global retention" )
1322+ // Create API key that expired long ago.
1323+ oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1324+ UserID : user .ID ,
1325+ ExpiresAt : tc .oldExpiredTime ,
1326+ TokenName : "old-expired-key" ,
1327+ })
13911328
1392- _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1393- require .NoError (t , err , "recently expired key should be kept" )
1394- })
1329+ // Create API key that expired recently if specified.
1330+ var recentExpiredKey database.APIKey
1331+ if tc .recentExpiredTime != nil {
1332+ recentExpiredKey , _ = dbgen .APIKey (t , db , database.APIKey {
1333+ UserID : user .ID ,
1334+ ExpiresAt : * tc .recentExpiredTime ,
1335+ TokenName : "recent-expired-key" ,
1336+ })
1337+ }
13951338
1396- t .Run ("CustomRetention30Days" , func (t * testing.T ) {
1397- t .Parallel ()
1339+ // Create API key that hasn't expired yet if specified.
1340+ var activeKey database.APIKey
1341+ if tc .activeTime != nil {
1342+ activeKey , _ = dbgen .APIKey (t , db , database.APIKey {
1343+ UserID : user .ID ,
1344+ ExpiresAt : * tc .activeTime ,
1345+ TokenName : "active-key" ,
1346+ })
1347+ }
13981348
1399- ctx := testutil .Context (t , testutil .WaitShort )
1349+ // Run the purge.
1350+ done := awaitDoTick (ctx , t , clk )
1351+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1352+ Retention : tc .retentionConfig ,
1353+ }, clk )
1354+ defer closer .Close ()
1355+ testutil .TryReceive (ctx , t , done )
14001356
1401- clk := quartz .NewMock (t )
1402- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1403- retentionPeriod := 30 * 24 * time .Hour // 30 days
1404- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // Expired 31 days ago (should be deleted)
1405- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour ) // Expired 29 days ago (should be kept)
1406- clk .Set (now ).MustWait (ctx )
1357+ // Verify total keys remaining.
1358+ keys , err := db .GetAPIKeysLastUsedAfter (ctx , time.Time {})
1359+ require .NoError (t , err )
1360+ require .Len (t , keys , tc .expectedKeysRemaining , "unexpected number of keys remaining" )
14071361
1408- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1409- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1410- user := dbgen .User (t , db , database.User {})
1362+ // Verify results.
1363+ _ , err = db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1364+ if tc .expectOldExpiredDeleted {
1365+ require .Error (t , err , "old expired key should be deleted" )
1366+ } else {
1367+ require .NoError (t , err , "old expired key should NOT be deleted" )
1368+ }
14111369
1412- // Create API key that expired long ago (should be deleted)
1413- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1414- UserID : user .ID ,
1415- ExpiresAt : expiredLongAgo ,
1416- TokenName : "old-expired-key" ,
1417- })
1370+ if tc .recentExpiredTime != nil {
1371+ _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1372+ require .NoError (t , err , "recently expired key should be kept" )
1373+ }
14181374
1419- // Create API key that expired recently (should be kept)
1420- recentExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1421- UserID : user .ID ,
1422- ExpiresAt : expiredRecently ,
1423- TokenName : "recent-expired-key" ,
1375+ if tc .activeTime != nil {
1376+ _ , err = db .GetAPIKeyByID (ctx , activeKey .ID )
1377+ require .NoError (t , err , "active key should be kept" )
1378+ }
14241379 })
1380+ }
1381+ }
14251382
1426- // Run the purge with 30-day retention
1427- done := awaitDoTick (ctx , t , clk )
1428- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1429- Retention : codersdk.RetentionConfig {
1430- APIKeys : serpent .Duration (retentionPeriod ),
1431- },
1432- }, clk )
1433- defer closer .Close ()
1434- testutil .TryReceive (ctx , t , done )
1435-
1436- // Verify results
1437- _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1438- require .Error (t , err , "old expired key should be deleted with 30-day retention" )
1439-
1440- _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1441- require .NoError (t , err , "recently expired key should be kept with 30-day retention" )
1442- })
1383+ // ptr is a helper to create a pointer to a value.
1384+ func ptr [T any ](v T ) * T {
1385+ return & v
14431386}
0 commit comments