@@ -1410,195 +1410,138 @@ func TestDeleteOldAuditLogs(t *testing.T) {
14101410func TestDeleteExpiredAPIKeys (t * testing.T ) {
14111411 t .Parallel ()
14121412
1413- t .Run ("RetentionEnabled" , func (t * testing.T ) {
1414- t .Parallel ()
1415-
1416- ctx := testutil .Context (t , testutil .WaitShort )
1417-
1418- clk := quartz .NewMock (t )
1419- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1420- retentionPeriod := 7 * 24 * time .Hour // 7 days
1421- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // Expired 8 days ago (should be deleted)
1422- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour ) // Expired 6 days ago (should be kept)
1423- notExpired := now .Add (24 * time .Hour ) // Expires tomorrow (should be kept)
1424- clk .Set (now ).MustWait (ctx )
1425-
1426- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1427- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1428- user := dbgen .User (t , db , database.User {})
1429-
1430- // Create API key that expired long ago (should be deleted)
1431- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1432- UserID : user .ID ,
1433- ExpiresAt : expiredLongAgo ,
1434- TokenName : "old-expired-key" ,
1435- })
1436-
1437- // Create API key that expired recently (should be kept)
1438- recentExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1439- UserID : user .ID ,
1440- ExpiresAt : expiredRecently ,
1441- TokenName : "recent-expired-key" ,
1442- })
1443-
1444- // Create API key that hasn't expired yet (should be kept)
1445- activeKey , _ := dbgen .APIKey (t , db , database.APIKey {
1446- UserID : user .ID ,
1447- ExpiresAt : notExpired ,
1448- TokenName : "active-key" ,
1449- })
1413+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
14501414
1451- // Run the purge with configured retention period
1452- done := awaitDoTick (ctx , t , clk )
1453- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1454- Retention : codersdk.RetentionConfig {
1455- APIKeys : serpent .Duration (retentionPeriod ),
1415+ testCases := []struct {
1416+ name string
1417+ retentionConfig codersdk.RetentionConfig
1418+ oldExpiredTime time.Time
1419+ recentExpiredTime * time.Time // nil means no recent expired key created
1420+ activeTime * time.Time // nil means no active key created
1421+ expectOldExpiredDeleted bool
1422+ expectedKeysRemaining int
1423+ }{
1424+ {
1425+ name : "RetentionEnabled" ,
1426+ retentionConfig : codersdk.RetentionConfig {
1427+ APIKeys : serpent .Duration (7 * 24 * time .Hour ), // 7 days
14561428 },
1457- }, clk )
1458- defer closer .Close ()
1459- testutil .TryReceive (ctx , t , done )
1460-
1461- // Verify results
1462- _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1463- require .Error (t , err , "old expired key should be deleted" )
1464-
1465- _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1466- require .NoError (t , err , "recently expired key should be kept" )
1467-
1468- _ , err = db .GetAPIKeyByID (ctx , activeKey .ID )
1469- require .NoError (t , err , "active key should be kept" )
1470- })
1471-
1472- t .Run ("RetentionDisabled" , func (t * testing.T ) {
1473- t .Parallel ()
1474-
1475- ctx := testutil .Context (t , testutil .WaitShort )
1476-
1477- clk := quartz .NewMock (t )
1478- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1479- expiredLongAgo := now .Add (- 365 * 24 * time .Hour ) // Expired 1 year ago
1480- clk .Set (now ).MustWait (ctx )
1481-
1482- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1483- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1484- user := dbgen .User (t , db , database.User {})
1485-
1486- // Create API key that expired long ago (should NOT be deleted when retention is 0)
1487- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1488- UserID : user .ID ,
1489- ExpiresAt : expiredLongAgo ,
1490- TokenName : "old-expired-key" ,
1491- })
1492-
1493- // Run the purge with retention disabled (0)
1494- done := awaitDoTick (ctx , t , clk )
1495- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1496- Retention : codersdk.RetentionConfig {
1497- APIKeys : serpent .Duration (0 ), // disabled
1429+ oldExpiredTime : now .Add (- 8 * 24 * time .Hour ), // Expired 8 days ago
1430+ recentExpiredTime : ptr (now .Add (- 6 * 24 * time .Hour )), // Expired 6 days ago
1431+ activeTime : ptr (now .Add (24 * time .Hour )), // Expires tomorrow
1432+ expectOldExpiredDeleted : true ,
1433+ expectedKeysRemaining : 2 , // recent expired + active
1434+ },
1435+ {
1436+ name : "RetentionDisabled" ,
1437+ retentionConfig : codersdk.RetentionConfig {
1438+ APIKeys : serpent .Duration (0 ),
14981439 },
1499- }, clk )
1500- defer closer .Close ()
1501- testutil .TryReceive (ctx , t , done )
1502-
1503- // Verify old expired key is still present
1504- _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1505- require .NoError (t , err , "old expired key should NOT be deleted when retention is disabled" )
1506- })
1507-
1508- t .Run ("GlobalRetentionFallback" , func (t * testing.T ) {
1509- t .Parallel ()
1510-
1511- ctx := testutil .Context (t , testutil .WaitShort )
1512-
1513- clk := quartz .NewMock (t )
1514- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1515- retentionPeriod := 14 * 24 * time .Hour // 14 days global
1516- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // Expired 15 days ago (should be deleted)
1517- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour ) // Expired 13 days ago (should be kept)
1518- clk .Set (now ).MustWait (ctx )
1519-
1520- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1521- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1522- user := dbgen .User (t , db , database.User {})
1523-
1524- // Create API key that expired long ago (should be deleted)
1525- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1526- UserID : user .ID ,
1527- ExpiresAt : expiredLongAgo ,
1528- TokenName : "old-expired-key" ,
1529- })
1530-
1531- // Create API key that expired recently (should be kept)
1532- recentExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1533- UserID : user .ID ,
1534- ExpiresAt : expiredRecently ,
1535- TokenName : "recent-expired-key" ,
1536- })
1537-
1538- // Run the purge with global retention (API keys retention is 0, so it falls back)
1539- done := awaitDoTick (ctx , t , clk )
1540- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1541- Retention : codersdk.RetentionConfig {
1542- Global : serpent .Duration (retentionPeriod ), // Use global
1543- APIKeys : serpent .Duration (0 ), // Not set, should fall back to global
1440+ oldExpiredTime : now .Add (- 365 * 24 * time .Hour ), // Expired 1 year ago
1441+ recentExpiredTime : nil ,
1442+ activeTime : nil ,
1443+ expectOldExpiredDeleted : false ,
1444+ expectedKeysRemaining : 1 , // old expired is kept
1445+ },
1446+ {
1447+ name : "GlobalRetentionFallback" ,
1448+ retentionConfig : codersdk.RetentionConfig {
1449+ Global : serpent .Duration (14 * 24 * time .Hour ), // 14 days global
1450+ APIKeys : serpent .Duration (0 ), // Not set, should fall back to global
15441451 },
1545- }, clk )
1546- defer closer .Close ()
1547- testutil .TryReceive (ctx , t , done )
1452+ oldExpiredTime : now .Add (- 15 * 24 * time .Hour ), // Expired 15 days ago
1453+ recentExpiredTime : ptr (now .Add (- 13 * 24 * time .Hour )), // Expired 13 days ago
1454+ activeTime : nil ,
1455+ expectOldExpiredDeleted : true ,
1456+ expectedKeysRemaining : 1 , // only recent expired remains
1457+ },
1458+ {
1459+ name : "CustomRetention30Days" ,
1460+ retentionConfig : codersdk.RetentionConfig {
1461+ APIKeys : serpent .Duration (30 * 24 * time .Hour ), // 30 days
1462+ },
1463+ oldExpiredTime : now .Add (- 31 * 24 * time .Hour ), // Expired 31 days ago
1464+ recentExpiredTime : ptr (now .Add (- 29 * 24 * time .Hour )), // Expired 29 days ago
1465+ activeTime : nil ,
1466+ expectOldExpiredDeleted : true ,
1467+ expectedKeysRemaining : 1 , // only recent expired remains
1468+ },
1469+ }
15481470
1549- // Verify results
1550- _ , err := db . GetAPIKeyByID ( ctx , oldExpiredKey . ID )
1551- require . Error ( t , err , "old expired key should be deleted via global retention" )
1471+ for _ , tc := range testCases {
1472+ t . Run ( tc . name , func ( t * testing. T ) {
1473+ t . Parallel ( )
15521474
1553- _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1554- require .NoError (t , err , "recently expired key should be kept" )
1555- })
1475+ ctx := testutil .Context (t , testutil .WaitShort )
1476+ clk := quartz .NewMock (t )
1477+ clk .Set (now ).MustWait (ctx )
1478+
1479+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1480+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1481+ user := dbgen .User (t , db , database.User {})
1482+
1483+ // Create API key that expired long ago.
1484+ oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1485+ UserID : user .ID ,
1486+ ExpiresAt : tc .oldExpiredTime ,
1487+ TokenName : "old-expired-key" ,
1488+ })
15561489
1557- t .Run ("CustomRetention30Days" , func (t * testing.T ) {
1558- t .Parallel ()
1490+ // Create API key that expired recently if specified.
1491+ var recentExpiredKey database.APIKey
1492+ if tc .recentExpiredTime != nil {
1493+ recentExpiredKey , _ = dbgen .APIKey (t , db , database.APIKey {
1494+ UserID : user .ID ,
1495+ ExpiresAt : * tc .recentExpiredTime ,
1496+ TokenName : "recent-expired-key" ,
1497+ })
1498+ }
15591499
1560- ctx := testutil .Context (t , testutil .WaitShort )
1500+ // Create API key that hasn't expired yet if specified.
1501+ var activeKey database.APIKey
1502+ if tc .activeTime != nil {
1503+ activeKey , _ = dbgen .APIKey (t , db , database.APIKey {
1504+ UserID : user .ID ,
1505+ ExpiresAt : * tc .activeTime ,
1506+ TokenName : "active-key" ,
1507+ })
1508+ }
15611509
1562- clk := quartz .NewMock (t )
1563- now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1564- retentionPeriod := 30 * 24 * time .Hour // 30 days
1565- expiredLongAgo := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // Expired 31 days ago (should be deleted)
1566- expiredRecently := now .Add (- retentionPeriod ).Add (24 * time .Hour ) // Expired 29 days ago (should be kept)
1567- clk .Set (now ).MustWait (ctx )
1510+ // Run the purge.
1511+ done := awaitDoTick (ctx , t , clk )
1512+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1513+ Retention : tc .retentionConfig ,
1514+ }, clk )
1515+ defer closer .Close ()
1516+ testutil .TryReceive (ctx , t , done )
15681517
1569- db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1570- logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1571- user := dbgen .User (t , db , database.User {})
1518+ // Verify total keys remaining.
1519+ keys , err := db .GetAPIKeysLastUsedAfter (ctx , time.Time {})
1520+ require .NoError (t , err )
1521+ require .Len (t , keys , tc .expectedKeysRemaining , "unexpected number of keys remaining" )
1522+
1523+ // Verify results.
1524+ _ , err = db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1525+ if tc .expectOldExpiredDeleted {
1526+ require .Error (t , err , "old expired key should be deleted" )
1527+ } else {
1528+ require .NoError (t , err , "old expired key should NOT be deleted" )
1529+ }
15721530
1573- // Create API key that expired long ago (should be deleted)
1574- oldExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1575- UserID : user .ID ,
1576- ExpiresAt : expiredLongAgo ,
1577- TokenName : "old-expired-key" ,
1578- })
1531+ if tc .recentExpiredTime != nil {
1532+ _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1533+ require .NoError (t , err , "recently expired key should be kept" )
1534+ }
15791535
1580- // Create API key that expired recently (should be kept)
1581- recentExpiredKey , _ := dbgen .APIKey (t , db , database.APIKey {
1582- UserID : user .ID ,
1583- ExpiresAt : expiredRecently ,
1584- TokenName : "recent-expired-key" ,
1536+ if tc .activeTime != nil {
1537+ _ , err = db .GetAPIKeyByID (ctx , activeKey .ID )
1538+ require .NoError (t , err , "active key should be kept" )
1539+ }
15851540 })
1541+ }
1542+ }
15861543
1587- // Run the purge with 30-day retention
1588- done := awaitDoTick (ctx , t , clk )
1589- closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1590- Retention : codersdk.RetentionConfig {
1591- APIKeys : serpent .Duration (retentionPeriod ),
1592- },
1593- }, clk )
1594- defer closer .Close ()
1595- testutil .TryReceive (ctx , t , done )
1596-
1597- // Verify results
1598- _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1599- require .Error (t , err , "old expired key should be deleted with 30-day retention" )
1600-
1601- _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1602- require .NoError (t , err , "recently expired key should be kept with 30-day retention" )
1603- })
1544+ // ptr is a helper to create a pointer to a value.
1545+ func ptr [T any ](v T ) * T {
1546+ return & v
16041547}
0 commit comments