@@ -1245,3 +1245,199 @@ func TestDeleteOldAuditLogs(t *testing.T) {
12451245 require .NotContains (t , logIDs , oldCreateLog .ID , "old create log should be deleted by audit logs retention" )
12461246 })
12471247}
1248+
1249+ func TestDeleteExpiredAPIKeys (t * testing.T ) {
1250+ t .Parallel ()
1251+
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+ })
1289+
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 ),
1295+ },
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
1337+ },
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 {})
1362+
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+ })
1369+
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+ })
1376+
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 )
1387+
1388+ // Verify results
1389+ _ , err := db .GetAPIKeyByID (ctx , oldExpiredKey .ID )
1390+ require .Error (t , err , "old expired key should be deleted via global retention" )
1391+
1392+ _ , err = db .GetAPIKeyByID (ctx , recentExpiredKey .ID )
1393+ require .NoError (t , err , "recently expired key should be kept" )
1394+ })
1395+
1396+ t .Run ("CustomRetention30Days" , func (t * testing.T ) {
1397+ t .Parallel ()
1398+
1399+ ctx := testutil .Context (t , testutil .WaitShort )
1400+
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 )
1407+
1408+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1409+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1410+ user := dbgen .User (t , db , database.User {})
1411+
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+ })
1418+
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" ,
1424+ })
1425+
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+ })
1443+ }
0 commit comments