@@ -1061,3 +1061,265 @@ func TestDeleteOldAIBridgeRecords(t *testing.T) {
10611061require .NoError (t ,err )
10621062require .Len (t ,newToolUsages ,1 ,"near threshold tool usages should not be deleted" )
10631063}
1064+
1065+ func TestDeleteOldAuditLogs (t * testing.T ) {
1066+ t .Parallel ()
1067+
1068+ t .Run ("RetentionEnabled" ,func (t * testing.T ) {
1069+ t .Parallel ()
1070+
1071+ ctx := testutil .Context (t ,testutil .WaitShort )
1072+
1073+ clk := quartz .NewMock (t )
1074+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1075+ retentionPeriod := 30 * 24 * time .Hour // 30 days
1076+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// 31 days ago (older than threshold)
1077+ beforeThreshold := now .Add (- 15 * 24 * time .Hour )// 15 days ago (newer than threshold)
1078+ clk .Set (now ).MustWait (ctx )
1079+
1080+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1081+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1082+ user := dbgen .User (t ,db , database.User {})
1083+ org := dbgen .Organization (t ,db , database.Organization {})
1084+
1085+ // Create old audit log (should be deleted)
1086+ oldLog := dbgen .AuditLog (t ,db , database.AuditLog {
1087+ UserID :user .ID ,
1088+ OrganizationID :org .ID ,
1089+ Time :afterThreshold ,
1090+ Action :database .AuditActionCreate ,
1091+ ResourceType :database .ResourceTypeWorkspace ,
1092+ })
1093+
1094+ // Create recent audit log (should be kept)
1095+ recentLog := dbgen .AuditLog (t ,db , database.AuditLog {
1096+ UserID :user .ID ,
1097+ OrganizationID :org .ID ,
1098+ Time :beforeThreshold ,
1099+ Action :database .AuditActionCreate ,
1100+ ResourceType :database .ResourceTypeWorkspace ,
1101+ })
1102+
1103+ // Run the purge with configured retention period
1104+ done := awaitDoTick (ctx ,t ,clk )
1105+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1106+ Retention : codersdk.RetentionConfig {
1107+ AuditLogs :serpent .Duration (retentionPeriod ),
1108+ },
1109+ },clk )
1110+ defer closer .Close ()
1111+ testutil .TryReceive (ctx ,t ,done )
1112+
1113+ // Verify results by querying all audit logs
1114+ logs ,err := db .GetAuditLogsOffset (ctx , database.GetAuditLogsOffsetParams {
1115+ LimitOpt :100 ,
1116+ })
1117+ require .NoError (t ,err )
1118+
1119+ logIDs := make ([]uuid.UUID ,len (logs ))
1120+ for i ,log := range logs {
1121+ logIDs [i ]= log .AuditLog .ID
1122+ }
1123+
1124+ require .NotContains (t ,logIDs ,oldLog .ID ,"old audit log should be deleted" )
1125+ require .Contains (t ,logIDs ,recentLog .ID ,"recent audit log should be kept" )
1126+ })
1127+
1128+ t .Run ("RetentionDisabled" ,func (t * testing.T ) {
1129+ t .Parallel ()
1130+
1131+ ctx := testutil .Context (t ,testutil .WaitShort )
1132+
1133+ clk := quartz .NewMock (t )
1134+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1135+ oldTime := now .Add (- 365 * 24 * time .Hour )// 1 year ago
1136+ clk .Set (now ).MustWait (ctx )
1137+
1138+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1139+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1140+ user := dbgen .User (t ,db , database.User {})
1141+ org := dbgen .Organization (t ,db , database.Organization {})
1142+
1143+ // Create old audit log (should NOT be deleted when retention is 0)
1144+ oldLog := dbgen .AuditLog (t ,db , database.AuditLog {
1145+ UserID :user .ID ,
1146+ OrganizationID :org .ID ,
1147+ Time :oldTime ,
1148+ Action :database .AuditActionCreate ,
1149+ ResourceType :database .ResourceTypeWorkspace ,
1150+ })
1151+
1152+ // Run the purge with retention disabled (0)
1153+ done := awaitDoTick (ctx ,t ,clk )
1154+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1155+ Retention : codersdk.RetentionConfig {
1156+ AuditLogs :serpent .Duration (0 ),// disabled
1157+ },
1158+ },clk )
1159+ defer closer .Close ()
1160+ testutil .TryReceive (ctx ,t ,done )
1161+
1162+ // Verify old log is still present
1163+ logs ,err := db .GetAuditLogsOffset (ctx , database.GetAuditLogsOffsetParams {
1164+ LimitOpt :100 ,
1165+ })
1166+ require .NoError (t ,err )
1167+
1168+ logIDs := make ([]uuid.UUID ,len (logs ))
1169+ for i ,log := range logs {
1170+ logIDs [i ]= log .AuditLog .ID
1171+ }
1172+
1173+ require .Contains (t ,logIDs ,oldLog .ID ,"old audit log should NOT be deleted when retention is disabled" )
1174+ })
1175+
1176+ t .Run ("GlobalRetentionFallback" ,func (t * testing.T ) {
1177+ t .Parallel ()
1178+
1179+ ctx := testutil .Context (t ,testutil .WaitShort )
1180+
1181+ clk := quartz .NewMock (t )
1182+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1183+ retentionPeriod := 30 * 24 * time .Hour // 30 days
1184+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// 31 days ago (older than threshold)
1185+ beforeThreshold := now .Add (- 15 * 24 * time .Hour )// 15 days ago (newer than threshold)
1186+ clk .Set (now ).MustWait (ctx )
1187+
1188+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1189+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1190+ user := dbgen .User (t ,db , database.User {})
1191+ org := dbgen .Organization (t ,db , database.Organization {})
1192+
1193+ // Create old audit log (should be deleted)
1194+ oldLog := dbgen .AuditLog (t ,db , database.AuditLog {
1195+ UserID :user .ID ,
1196+ OrganizationID :org .ID ,
1197+ Time :afterThreshold ,
1198+ Action :database .AuditActionCreate ,
1199+ ResourceType :database .ResourceTypeWorkspace ,
1200+ })
1201+
1202+ // Create recent audit log (should be kept)
1203+ recentLog := dbgen .AuditLog (t ,db , database.AuditLog {
1204+ UserID :user .ID ,
1205+ OrganizationID :org .ID ,
1206+ Time :beforeThreshold ,
1207+ Action :database .AuditActionCreate ,
1208+ ResourceType :database .ResourceTypeWorkspace ,
1209+ })
1210+
1211+ // Run the purge with global retention (audit logs retention is 0, so it falls back)
1212+ done := awaitDoTick (ctx ,t ,clk )
1213+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1214+ Retention : codersdk.RetentionConfig {
1215+ Global :serpent .Duration (retentionPeriod ),// Use global
1216+ AuditLogs :serpent .Duration (0 ),// Not set, should fall back to global
1217+ },
1218+ },clk )
1219+ defer closer .Close ()
1220+ testutil .TryReceive (ctx ,t ,done )
1221+
1222+ // Verify results
1223+ logs ,err := db .GetAuditLogsOffset (ctx , database.GetAuditLogsOffsetParams {
1224+ LimitOpt :100 ,
1225+ })
1226+ require .NoError (t ,err )
1227+
1228+ logIDs := make ([]uuid.UUID ,len (logs ))
1229+ for i ,log := range logs {
1230+ logIDs [i ]= log .AuditLog .ID
1231+ }
1232+
1233+ require .NotContains (t ,logIDs ,oldLog .ID ,"old audit log should be deleted via global retention" )
1234+ require .Contains (t ,logIDs ,recentLog .ID ,"recent audit log should be kept" )
1235+ })
1236+
1237+ t .Run ("ConnectionEventsNotDeleted" ,func (t * testing.T ) {
1238+ t .Parallel ()
1239+
1240+ ctx := testutil .Context (t ,testutil .WaitShort )
1241+
1242+ clk := quartz .NewMock (t )
1243+ now := time .Date (2025 ,1 ,15 ,7 ,30 ,0 ,0 ,time .UTC )
1244+ retentionPeriod := 30 * 24 * time .Hour // 30 days
1245+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour )// 31 days ago (older than threshold)
1246+ clk .Set (now ).MustWait (ctx )
1247+
1248+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
1249+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
1250+ user := dbgen .User (t ,db , database.User {})
1251+ org := dbgen .Organization (t ,db , database.Organization {})
1252+
1253+ // Create old connection events (should NOT be deleted by audit logs retention)
1254+ oldConnectLog := dbgen .AuditLog (t ,db , database.AuditLog {
1255+ UserID :user .ID ,
1256+ OrganizationID :org .ID ,
1257+ Time :afterThreshold ,
1258+ Action :database .AuditActionConnect ,
1259+ ResourceType :database .ResourceTypeWorkspace ,
1260+ })
1261+
1262+ oldDisconnectLog := dbgen .AuditLog (t ,db , database.AuditLog {
1263+ UserID :user .ID ,
1264+ OrganizationID :org .ID ,
1265+ Time :afterThreshold ,
1266+ Action :database .AuditActionDisconnect ,
1267+ ResourceType :database .ResourceTypeWorkspace ,
1268+ })
1269+
1270+ oldOpenLog := dbgen .AuditLog (t ,db , database.AuditLog {
1271+ UserID :user .ID ,
1272+ OrganizationID :org .ID ,
1273+ Time :afterThreshold ,
1274+ Action :database .AuditActionOpen ,
1275+ ResourceType :database .ResourceTypeWorkspace ,
1276+ })
1277+
1278+ oldCloseLog := dbgen .AuditLog (t ,db , database.AuditLog {
1279+ UserID :user .ID ,
1280+ OrganizationID :org .ID ,
1281+ Time :afterThreshold ,
1282+ Action :database .AuditActionClose ,
1283+ ResourceType :database .ResourceTypeWorkspace ,
1284+ })
1285+
1286+ // Create old non-connection audit log (should be deleted)
1287+ oldCreateLog := dbgen .AuditLog (t ,db , database.AuditLog {
1288+ UserID :user .ID ,
1289+ OrganizationID :org .ID ,
1290+ Time :afterThreshold ,
1291+ Action :database .AuditActionCreate ,
1292+ ResourceType :database .ResourceTypeWorkspace ,
1293+ })
1294+
1295+ // Run the purge with audit logs retention enabled
1296+ done := awaitDoTick (ctx ,t ,clk )
1297+ closer := dbpurge .New (ctx ,logger ,db ,& codersdk.DeploymentValues {
1298+ Retention : codersdk.RetentionConfig {
1299+ AuditLogs :serpent .Duration (retentionPeriod ),
1300+ },
1301+ },clk )
1302+ defer closer .Close ()
1303+ testutil .TryReceive (ctx ,t ,done )
1304+
1305+ // Verify results
1306+ logs ,err := db .GetAuditLogsOffset (ctx , database.GetAuditLogsOffsetParams {
1307+ LimitOpt :100 ,
1308+ })
1309+ require .NoError (t ,err )
1310+
1311+ logIDs := make ([]uuid.UUID ,len (logs ))
1312+ for i ,log := range logs {
1313+ logIDs [i ]= log .AuditLog .ID
1314+ }
1315+
1316+ // Connection events should NOT be deleted by audit logs retention
1317+ require .Contains (t ,logIDs ,oldConnectLog .ID ,"old connect log should NOT be deleted by audit logs retention" )
1318+ require .Contains (t ,logIDs ,oldDisconnectLog .ID ,"old disconnect log should NOT be deleted by audit logs retention" )
1319+ require .Contains (t ,logIDs ,oldOpenLog .ID ,"old open log should NOT be deleted by audit logs retention" )
1320+ require .Contains (t ,logIDs ,oldCloseLog .ID ,"old close log should NOT be deleted by audit logs retention" )
1321+
1322+ // Non-connection event should be deleted
1323+ require .NotContains (t ,logIDs ,oldCreateLog .ID ,"old create log should be deleted by audit logs retention" )
1324+ })
1325+ }