@@ -1410,195 +1410,138 @@ func TestDeleteOldAuditLogs(t *testing.T) {
14101410func TestDeleteExpiredAPIKeys (t * testing.T ) {
14111411t .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}