@@ -1406,3 +1406,199 @@ func TestDeleteOldAuditLogs(t *testing.T) {
14061406 require .NotContains (t , logIDs , oldCreateLog .ID , "old create log should be deleted by audit logs retention" )
14071407 })
14081408}
1409+
1410+ func TestDeleteExpiredAPIKeys (t * testing.T ) {
1411+ t .Parallel ()
1412+
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+ })
1450+
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 ),
1456+ },
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
1498+ },
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
1544+ },
1545+ }, clk )
1546+ defer closer .Close ()
1547+ testutil .TryReceive (ctx , t , done )
1548+
1549+ // Verify results
1550+ _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1551+ require .Error (t , err , "old expired key should be deleted via global retention" )
1552+
1553+ _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1554+ require .NoError (t , err , "recently expired key should be kept" )
1555+ })
1556+
1557+ t .Run ("CustomRetention30Days" , func (t * testing.T ) {
1558+ t .Parallel ()
1559+
1560+ ctx := testutil .Context (t , testutil .WaitShort )
1561+
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 )
1568+
1569+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1570+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1571+ user := dbgen .User (t , db , database.User {})
1572+
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+ })
1579+
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" ,
1585+ })
1586+
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+ })
1604+ }
0 commit comments