@@ -1271,195 +1271,138 @@ func TestDeleteOldAuditLogs(t *testing.T) {
12711271func TestDeleteExpiredAPIKeys (t * testing.T ) {
12721272t .Parallel ()
12731273
1274- t .Run ("RetentionEnabled" ,func (t * testing.T ) {
1275- t .Parallel ()
1276-
1277- ctx := testutil .Context (t ,testutil .WaitShort )
1278-
1279- clk := quartz .NewMock (t )
1280- now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1281- retentionPeriod := 7 * 24 * time .Hour // 7 days
1282- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// Expired 8 days ago (should be deleted)
1283- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour )// Expired 6 days ago (should be kept)
1284- notExpired := now .Add (24 * time .Hour )// Expires tomorrow (should be kept)
1285- clk .Set (now ).MustWait (ctx )
1286-
1287- db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1288- logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1289- user := dbgen .User (t ,db , database.User {})
1290-
1291- // Create API key that expired long ago (should be deleted)
1292- oldExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1293- UserID :user .ID ,
1294- ExpiresAt :expiredLongAgo ,
1295- TokenName :"old-expired-key" ,
1296- })
1297-
1298- // Create API key that expired recently (should be kept)
1299- recentExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1300- UserID :user .ID ,
1301- ExpiresAt :expiredRecently ,
1302- TokenName :"recent-expired-key" ,
1303- })
1304-
1305- // Create API key that hasn't expired yet (should be kept)
1306- activeKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1307- UserID :user .ID ,
1308- ExpiresAt :notExpired ,
1309- TokenName :"active-key" ,
1310- })
1274+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
13111275
1312- // Run the purge with configured retention period
1313- done := awaitDoTick (ctx ,t ,clk )
1314- closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1315- Retention : codersdk.RetentionConfig {
1316- APIKeys :serpent .Duration (retentionPeriod ),
1276+ testCases := []struct {
1277+ name string
1278+ retentionConfig codersdk.RetentionConfig
1279+ oldExpiredTime time.Time
1280+ recentExpiredTime * time.Time // nil means no recent expired key created
1281+ activeTime * time.Time // nil means no active key created
1282+ expectOldExpiredDeleted bool
1283+ expectedKeysRemaining int
1284+ }{
1285+ {
1286+ name :"RetentionEnabled" ,
1287+ retentionConfig : codersdk.RetentionConfig {
1288+ APIKeys :serpent .Duration (7 * 24 * time .Hour ),// 7 days
13171289},
1318- },clk )
1319- defer closer .Close ()
1320- testutil .TryReceive (ctx ,t ,done )
1321-
1322- // Verify results
1323- _ ,err := db .GetAPIKeyByID (ctx ,oldExpiredKey .ID )
1324- require .Error (t ,err ,"old expired key should be deleted" )
1325-
1326- _ ,err = db .GetAPIKeyByID (ctx ,recentExpiredKey .ID )
1327- require .NoError (t ,err ,"recently expired key should be kept" )
1328-
1329- _ ,err = db .GetAPIKeyByID (ctx ,activeKey .ID )
1330- require .NoError (t ,err ,"active key should be kept" )
1331- })
1332-
1333- t .Run ("RetentionDisabled" ,func (t * testing.T ) {
1334- t .Parallel ()
1335-
1336- ctx := testutil .Context (t ,testutil .WaitShort )
1337-
1338- clk := quartz .NewMock (t )
1339- now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1340- expiredLongAgo := now .Add (- 365 * 24 * time .Hour )// Expired 1 year ago
1341- clk .Set (now ).MustWait (ctx )
1342-
1343- db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1344- logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1345- user := dbgen .User (t ,db , database.User {})
1346-
1347- // Create API key that expired long ago (should NOT be deleted when retention is 0)
1348- oldExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1349- UserID :user .ID ,
1350- ExpiresAt :expiredLongAgo ,
1351- TokenName :"old-expired-key" ,
1352- })
1353-
1354- // Run the purge with retention disabled (0)
1355- done := awaitDoTick (ctx ,t ,clk )
1356- closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1357- Retention : codersdk.RetentionConfig {
1358- APIKeys :serpent .Duration (0 ),// disabled
1290+ oldExpiredTime :now .Add (- 8 * 24 * time .Hour ),// Expired 8 days ago
1291+ recentExpiredTime :ptr (now .Add (- 6 * 24 * time .Hour )),// Expired 6 days ago
1292+ activeTime :ptr (now .Add (24 * time .Hour )),// Expires tomorrow
1293+ expectOldExpiredDeleted :true ,
1294+ expectedKeysRemaining :2 ,// recent expired + active
1295+ },
1296+ {
1297+ name :"RetentionDisabled" ,
1298+ retentionConfig : codersdk.RetentionConfig {
1299+ APIKeys :serpent .Duration (0 ),
13591300},
1360- },clk )
1361- defer closer .Close ()
1362- testutil .TryReceive (ctx ,t ,done )
1363-
1364- // Verify old expired key is still present
1365- _ ,err := db .GetAPIKeyByID (ctx ,oldExpiredKey .ID )
1366- require .NoError (t ,err ,"old expired key should NOT be deleted when retention is disabled" )
1367- })
1368-
1369- t .Run ("GlobalRetentionFallback" ,func (t * testing.T ) {
1370- t .Parallel ()
1371-
1372- ctx := testutil .Context (t ,testutil .WaitShort )
1373-
1374- clk := quartz .NewMock (t )
1375- now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1376- retentionPeriod := 14 * 24 * time .Hour // 14 days global
1377- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// Expired 15 days ago (should be deleted)
1378- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour )// Expired 13 days ago (should be kept)
1379- clk .Set (now ).MustWait (ctx )
1380-
1381- db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1382- logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1383- user := dbgen .User (t ,db , database.User {})
1301+ oldExpiredTime :now .Add (- 365 * 24 * time .Hour ),// Expired 1 year ago
1302+ recentExpiredTime :nil ,
1303+ activeTime :nil ,
1304+ expectOldExpiredDeleted :false ,
1305+ expectedKeysRemaining :1 ,// old expired is kept
1306+ },
1307+ {
1308+ name :"GlobalRetentionFallback" ,
1309+ retentionConfig : codersdk.RetentionConfig {
1310+ Global :serpent .Duration (14 * 24 * time .Hour ),// 14 days global
1311+ APIKeys :serpent .Duration (0 ),// Not set, should fall back to global
1312+ },
1313+ oldExpiredTime :now .Add (- 15 * 24 * time .Hour ),// Expired 15 days ago
1314+ recentExpiredTime :ptr (now .Add (- 13 * 24 * time .Hour )),// Expired 13 days ago
1315+ activeTime :nil ,
1316+ expectOldExpiredDeleted :true ,
1317+ expectedKeysRemaining :1 ,// only recent expired remains
1318+ },
1319+ {
1320+ name :"CustomRetention30Days" ,
1321+ retentionConfig : codersdk.RetentionConfig {
1322+ APIKeys :serpent .Duration (30 * 24 * time .Hour ),// 30 days
1323+ },
1324+ oldExpiredTime :now .Add (- 31 * 24 * time .Hour ),// Expired 31 days ago
1325+ recentExpiredTime :ptr (now .Add (- 29 * 24 * time .Hour )),// Expired 29 days ago
1326+ activeTime :nil ,
1327+ expectOldExpiredDeleted :true ,
1328+ expectedKeysRemaining :1 ,// only recent expired remains
1329+ },
1330+ }
13841331
1385- // Create API key that expired long ago (should be deleted)
1386- oldExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1387- UserID :user .ID ,
1388- ExpiresAt :expiredLongAgo ,
1389- TokenName :"old-expired-key" ,
1390- })
1332+ for _ ,tc := range testCases {
1333+ t .Run (tc .name ,func (t * testing.T ) {
1334+ t .Parallel ()
13911335
1392- // Create API key that expired recently (should be kept)
1393- recentExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1394- UserID :user .ID ,
1395- ExpiresAt :expiredRecently ,
1396- TokenName :"recent-expired-key" ,
1397- })
1336+ ctx := testutil .Context (t ,testutil .WaitShort )
1337+ clk := quartz .NewMock (t )
1338+ clk .Set (now ).MustWait (ctx )
13981339
1399- // Run the purge with global retention (API keys retention is 0, so it falls back)
1400- done := awaitDoTick (ctx ,t ,clk )
1401- closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1402- Retention : codersdk.RetentionConfig {
1403- Global :serpent .Duration (retentionPeriod ),// Use global
1404- APIKeys :serpent .Duration (0 ),// Not set, should fall back to global
1405- },
1406- },clk )
1407- defer closer .Close ()
1408- testutil .TryReceive (ctx ,t ,done )
1340+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1341+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1342+ user := dbgen .User (t ,db , database.User {})
14091343
1410- // Verify results
1411- _ ,err := db .GetAPIKeyByID (ctx ,oldExpiredKey .ID )
1412- require .Error (t ,err ,"old expired key should be deleted via global retention" )
1344+ // Create API key that expired long ago.
1345+ oldExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1346+ UserID :user .ID ,
1347+ ExpiresAt :tc .oldExpiredTime ,
1348+ TokenName :"old-expired-key" ,
1349+ })
14131350
1414- _ ,err = db .GetAPIKeyByID (ctx ,recentExpiredKey .ID )
1415- require .NoError (t ,err ,"recently expired key should be kept" )
1416- })
1351+ // Create API key that expired recently if specified.
1352+ var recentExpiredKey database.APIKey
1353+ if tc .recentExpiredTime != nil {
1354+ recentExpiredKey ,_ = dbgen .APIKey (t ,db , database.APIKey {
1355+ UserID :user .ID ,
1356+ ExpiresAt :* tc .recentExpiredTime ,
1357+ TokenName :"recent-expired-key" ,
1358+ })
1359+ }
14171360
1418- t .Run ("CustomRetention30Days" ,func (t * testing.T ) {
1419- t .Parallel ()
1361+ // Create API key that hasn't expired yet if specified.
1362+ var activeKey database.APIKey
1363+ if tc .activeTime != nil {
1364+ activeKey ,_ = dbgen .APIKey (t ,db , database.APIKey {
1365+ UserID :user .ID ,
1366+ ExpiresAt :* tc .activeTime ,
1367+ TokenName :"active-key" ,
1368+ })
1369+ }
14201370
1421- ctx := testutil .Context (t ,testutil .WaitShort )
1371+ // Run the purge.
1372+ done := awaitDoTick (ctx ,t ,clk )
1373+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1374+ Retention :tc .retentionConfig ,
1375+ },clk )
1376+ defer closer .Close ()
1377+ testutil .TryReceive (ctx ,t ,done )
14221378
1423- clk := quartz .NewMock (t )
1424- now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1425- retentionPeriod := 30 * 24 * time .Hour // 30 days
1426- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// Expired 31 days ago (should be deleted)
1427- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour )// Expired 29 days ago (should be kept)
1428- clk .Set (now ).MustWait (ctx )
1379+ // Verify total keys remaining.
1380+ keys ,err := db .GetAPIKeysLastUsedAfter (ctx , time.Time {})
1381+ require .NoError (t ,err )
1382+ require .Len (t ,keys ,tc .expectedKeysRemaining ,"unexpected number of keys remaining" )
14291383
1430- db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1431- logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1432- user := dbgen .User (t ,db , database.User {})
1384+ // Verify results.
1385+ _ ,err = db .GetAPIKeyByID (ctx ,oldExpiredKey .ID )
1386+ if tc .expectOldExpiredDeleted {
1387+ require .Error (t ,err ,"old expired key should be deleted" )
1388+ }else {
1389+ require .NoError (t ,err ,"old expired key should NOT be deleted" )
1390+ }
14331391
1434- // Create API key that expired long ago (should be deleted)
1435- oldExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1436- UserID :user .ID ,
1437- ExpiresAt :expiredLongAgo ,
1438- TokenName :"old-expired-key" ,
1439- })
1392+ if tc .recentExpiredTime != nil {
1393+ _ ,err = db .GetAPIKeyByID (ctx ,recentExpiredKey .ID )
1394+ require .NoError (t ,err ,"recently expired key should be kept" )
1395+ }
14401396
1441- // Create API key that expired recently (should be kept)
1442- recentExpiredKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
1443- UserID :user .ID ,
1444- ExpiresAt :expiredRecently ,
1445- TokenName :"recent-expired-key" ,
1397+ if tc .activeTime != nil {
1398+ _ ,err = db .GetAPIKeyByID (ctx ,activeKey .ID )
1399+ require .NoError (t ,err ,"active key should be kept" )
1400+ }
14461401})
1402+ }
1403+ }
14471404
1448- // Run the purge with 30-day retention
1449- done := awaitDoTick (ctx ,t ,clk )
1450- closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1451- Retention : codersdk.RetentionConfig {
1452- APIKeys :serpent .Duration (retentionPeriod ),
1453- },
1454- },clk )
1455- defer closer .Close ()
1456- testutil .TryReceive (ctx ,t ,done )
1457-
1458- // Verify results
1459- _ ,err := db .GetAPIKeyByID (ctx ,oldExpiredKey .ID )
1460- require .Error (t ,err ,"old expired key should be deleted with 30-day retention" )
1461-
1462- _ ,err = db .GetAPIKeyByID (ctx ,recentExpiredKey .ID )
1463- require .NoError (t ,err ,"recently expired key should be kept with 30-day retention" )
1464- })
1405+ // ptr is a helper to create a pointer to a value.
1406+ func ptr [T any ](v T )* T {
1407+ return & v
14651408}