@@ -639,7 +639,6 @@ func TestDeleteOldAuditLogConnectionEventsLimit(t *testing.T) {
639639
640640require .Len (t ,logs ,0 )
641641}
642-
643642func TestExpireOldAPIKeys (t * testing.T ) {
644643t .Parallel ()
645644
@@ -704,3 +703,216 @@ func TestExpireOldAPIKeys(t *testing.T) {
704703// Out of an abundance of caution, we do not expire explicitly named prebuilds API keys.
705704assertKeyActive (namedPrebuildsAPIKey .ID )
706705}
706+
707+ //nolint:paralleltest // It uses LockIDDBPurge.
708+ func TestDeleteExpiredOAuth2ProviderAppCodes (t * testing.T ) {
709+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
710+ defer cancel ()
711+
712+ clk := quartz .NewMock (t )
713+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
714+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
715+
716+ now := dbtime .Now ()
717+ clk .Set (now ).MustWait (ctx )
718+
719+ // Create test data
720+ user := dbgen .User (t ,db , database.User {})
721+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
722+ Name :fmt .Sprintf ("test-codes-%d" ,time .Now ().UnixNano ()),
723+ })
724+
725+ // Create expired authorization code (should be deleted)
726+ expiredCode := dbgen .OAuth2ProviderAppCode (t ,db , database.OAuth2ProviderAppCode {
727+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
728+ AppID :app .ID ,
729+ UserID :user .ID ,
730+ SecretPrefix : []byte (fmt .Sprintf ("expired-%d" ,time .Now ().UnixNano ())),
731+ })
732+
733+ // Create non-expired authorization code (should be retained)
734+ validCode := dbgen .OAuth2ProviderAppCode (t ,db , database.OAuth2ProviderAppCode {
735+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
736+ AppID :app .ID ,
737+ UserID :user .ID ,
738+ SecretPrefix : []byte (fmt .Sprintf ("valid-%d" ,time .Now ().UnixNano ())),
739+ })
740+
741+ // Verify codes exist initially
742+ _ ,err := db .GetOAuth2ProviderAppCodeByID (ctx ,expiredCode .ID )
743+ require .NoError (t ,err )
744+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,validCode .ID )
745+ require .NoError (t ,err )
746+
747+ // Run cleanup
748+ done := awaitDoTick (ctx ,t ,clk )
749+ closer := dbpurge .New (ctx ,logger ,db ,clk )
750+ defer closer .Close ()
751+ <- done
752+
753+ // Verify expired code is deleted
754+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,expiredCode .ID )
755+ require .Error (t ,err )
756+ require .ErrorIs (t ,err ,sql .ErrNoRows )
757+
758+ // Verify non-expired code is retained
759+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,validCode .ID )
760+ require .NoError (t ,err )
761+ }
762+
763+ //nolint:paralleltest // It uses LockIDDBPurge.
764+ func TestDeleteExpiredOAuth2ProviderAppTokens (t * testing.T ) {
765+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
766+ defer cancel ()
767+
768+ clk := quartz .NewMock (t )
769+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
770+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
771+
772+ now := dbtime .Now ()
773+ clk .Set (now ).MustWait (ctx )
774+
775+ // Create test data
776+ user := dbgen .User (t ,db , database.User {})
777+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
778+ Name :fmt .Sprintf ("test-tokens-%d" ,time .Now ().UnixNano ()),
779+ })
780+ appSecret := dbgen .OAuth2ProviderAppSecret (t ,db , database.OAuth2ProviderAppSecret {
781+ AppID :app .ID ,
782+ })
783+
784+ // Create API keys for the tokens
785+ expiredAPIKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
786+ UserID :user .ID ,
787+ ExpiresAt :now .Add (- 1 * time .Hour ),
788+ })
789+ validAPIKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
790+ UserID :user .ID ,
791+ ExpiresAt :now .Add (24 * time .Hour ),// Valid for 24 hours
792+ })
793+
794+ // Create expired access token (should be deleted)
795+ expiredToken := dbgen .OAuth2ProviderAppToken (t ,db , database.OAuth2ProviderAppToken {
796+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
797+ AppSecretID :appSecret .ID ,
798+ APIKeyID :expiredAPIKey .ID ,
799+ UserID :user .ID ,
800+ HashPrefix : []byte (fmt .Sprintf ("expired-%d" ,time .Now ().UnixNano ())),
801+ })
802+
803+ // Create non-expired access token (should be retained)
804+ validToken := dbgen .OAuth2ProviderAppToken (t ,db , database.OAuth2ProviderAppToken {
805+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
806+ AppSecretID :appSecret .ID ,
807+ APIKeyID :validAPIKey .ID ,
808+ UserID :user .ID ,
809+ HashPrefix : []byte (fmt .Sprintf ("valid-%d" ,time .Now ().UnixNano ())),
810+ })
811+
812+ // Verify tokens exist initially
813+ _ ,err := db .GetOAuth2ProviderAppTokenByPrefix (ctx ,expiredToken .HashPrefix )
814+ require .NoError (t ,err )
815+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,validToken .HashPrefix )
816+ require .NoError (t ,err )
817+
818+ // Run cleanup
819+ done := awaitDoTick (ctx ,t ,clk )
820+ closer := dbpurge .New (ctx ,logger ,db ,clk )
821+ defer closer .Close ()
822+ <- done
823+
824+ // Verify expired token is deleted
825+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,expiredToken .HashPrefix )
826+ require .Error (t ,err )
827+ require .ErrorIs (t ,err ,sql .ErrNoRows )
828+
829+ // Verify non-expired token is retained
830+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,validToken .HashPrefix )
831+ require .NoError (t ,err )
832+ }
833+
834+ //nolint:paralleltest // It uses LockIDDBPurge.
835+ func TestDeleteExpiredOAuth2ProviderDeviceCodes (t * testing.T ) {
836+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
837+ defer cancel ()
838+
839+ clk := quartz .NewMock (t )
840+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
841+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
842+
843+ now := dbtime .Now ()
844+ clk .Set (now ).MustWait (ctx )
845+
846+ // Create test data
847+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
848+ Name :fmt .Sprintf ("test-device-%d" ,time .Now ().UnixNano ()),
849+ })
850+
851+ nanoTime := time .Now ().UnixNano ()
852+
853+ // Create expired device code with pending status (should be deleted)
854+ expiredPendingCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
855+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
856+ ClientID :app .ID ,
857+ Status :database .OAuth2DeviceStatusPending ,
858+ DeviceCodePrefix :fmt .Sprintf ("EP%06d" ,nanoTime % 1000000 ),
859+ UserCode :fmt .Sprintf ("EP%06d" ,nanoTime % 1000000 ),
860+ DeviceCodeHash :fmt .Appendf (nil ,"hash-exp-pending-%d" ,nanoTime ),
861+ })
862+
863+ // Create non-expired device code with pending status (should be retained)
864+ validPendingCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
865+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
866+ ClientID :app .ID ,
867+ Status :database .OAuth2DeviceStatusPending ,
868+ DeviceCodePrefix :fmt .Sprintf ("VP%06d" , (nanoTime + 1 )% 1000000 ),
869+ UserCode :fmt .Sprintf ("VP%06d" , (nanoTime + 1 )% 1000000 ),
870+ DeviceCodeHash :fmt .Appendf (nil ,"hash-val-pending-%d" ,nanoTime + 1 ),
871+ })
872+
873+ // Create expired device code with authorized status (should be deleted - all expired codes are deleted)
874+ expiredAuthorizedCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
875+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
876+ ClientID :app .ID ,
877+ DeviceCodePrefix :fmt .Sprintf ("EA%06d" , (nanoTime + 2 )% 1000000 ),
878+ UserCode :fmt .Sprintf ("EA%06d" , (nanoTime + 2 )% 1000000 ),
879+ DeviceCodeHash :fmt .Appendf (nil ,"hash-exp-auth-%d" ,nanoTime + 2 ),
880+ })
881+
882+ // Create a user and authorize the device code
883+ user := dbgen .User (t ,db , database.User {})
884+ expiredAuthorizedCode ,err := db .UpdateOAuth2ProviderDeviceCodeAuthorization (ctx , database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams {
885+ ID :expiredAuthorizedCode .ID ,
886+ UserID : uuid.NullUUID {UUID :user .ID ,Valid :true },
887+ Status :database .OAuth2DeviceStatusAuthorized ,
888+ })
889+ require .NoError (t ,err )
890+
891+ // Verify device codes exist initially
892+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredPendingCode .ID )
893+ require .NoError (t ,err )
894+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,validPendingCode .ID )
895+ require .NoError (t ,err )
896+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredAuthorizedCode .ID )
897+ require .NoError (t ,err )
898+
899+ // Run cleanup
900+ done := awaitDoTick (ctx ,t ,clk )
901+ closer := dbpurge .New (ctx ,logger ,db ,clk )
902+ defer closer .Close ()
903+ <- done
904+
905+ // Verify expired pending device code is deleted
906+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredPendingCode .ID )
907+ require .Error (t ,err )
908+ require .ErrorIs (t ,err ,sql .ErrNoRows )
909+
910+ // Verify non-expired pending device code is retained
911+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,validPendingCode .ID )
912+ require .NoError (t ,err )
913+
914+ // Verify expired authorized device code is deleted (all expired codes are deleted)
915+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredAuthorizedCode .ID )
916+ require .Error (t ,err )
917+ require .ErrorIs (t ,err ,sql .ErrNoRows )
918+ }