@@ -635,3 +635,216 @@ func TestDeleteOldAuditLogConnectionEventsLimit(t *testing.T) {
635635
636636require .Len (t ,logs ,0 )
637637}
638+
639+ //nolint:paralleltest // It uses LockIDDBPurge.
640+ func TestDeleteExpiredOAuth2ProviderAppCodes (t * testing.T ) {
641+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
642+ defer cancel ()
643+
644+ clk := quartz .NewMock (t )
645+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
646+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
647+
648+ now := dbtime .Now ()
649+ clk .Set (now ).MustWait (ctx )
650+
651+ // Create test data
652+ user := dbgen .User (t ,db , database.User {})
653+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
654+ Name :fmt .Sprintf ("test-codes-%d" ,time .Now ().UnixNano ()),
655+ })
656+
657+ // Create expired authorization code (should be deleted)
658+ expiredCode := dbgen .OAuth2ProviderAppCode (t ,db , database.OAuth2ProviderAppCode {
659+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
660+ AppID :app .ID ,
661+ UserID :user .ID ,
662+ SecretPrefix : []byte (fmt .Sprintf ("expired-%d" ,time .Now ().UnixNano ())),
663+ })
664+
665+ // Create non-expired authorization code (should be retained)
666+ validCode := dbgen .OAuth2ProviderAppCode (t ,db , database.OAuth2ProviderAppCode {
667+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
668+ AppID :app .ID ,
669+ UserID :user .ID ,
670+ SecretPrefix : []byte (fmt .Sprintf ("valid-%d" ,time .Now ().UnixNano ())),
671+ })
672+
673+ // Verify codes exist initially
674+ _ ,err := db .GetOAuth2ProviderAppCodeByID (ctx ,expiredCode .ID )
675+ require .NoError (t ,err )
676+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,validCode .ID )
677+ require .NoError (t ,err )
678+
679+ // Run cleanup
680+ done := awaitDoTick (ctx ,t ,clk )
681+ closer := dbpurge .New (ctx ,logger ,db ,clk )
682+ defer closer .Close ()
683+ <- done
684+
685+ // Verify expired code is deleted
686+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,expiredCode .ID )
687+ require .Error (t ,err )
688+ require .ErrorIs (t ,err ,sql .ErrNoRows )
689+
690+ // Verify non-expired code is retained
691+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,validCode .ID )
692+ require .NoError (t ,err )
693+ }
694+
695+ //nolint:paralleltest // It uses LockIDDBPurge.
696+ func TestDeleteExpiredOAuth2ProviderAppTokens (t * testing.T ) {
697+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
698+ defer cancel ()
699+
700+ clk := quartz .NewMock (t )
701+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
702+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
703+
704+ now := dbtime .Now ()
705+ clk .Set (now ).MustWait (ctx )
706+
707+ // Create test data
708+ user := dbgen .User (t ,db , database.User {})
709+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
710+ Name :fmt .Sprintf ("test-tokens-%d" ,time .Now ().UnixNano ()),
711+ })
712+ appSecret := dbgen .OAuth2ProviderAppSecret (t ,db , database.OAuth2ProviderAppSecret {
713+ AppID :app .ID ,
714+ })
715+
716+ // Create API keys for the tokens
717+ expiredAPIKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
718+ UserID :user .ID ,
719+ ExpiresAt :now .Add (- 1 * time .Hour ),
720+ })
721+ validAPIKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
722+ UserID :user .ID ,
723+ ExpiresAt :now .Add (24 * time .Hour ),// Valid for 24 hours
724+ })
725+
726+ // Create expired access token (should be deleted)
727+ expiredToken := dbgen .OAuth2ProviderAppToken (t ,db , database.OAuth2ProviderAppToken {
728+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
729+ AppSecretID :appSecret .ID ,
730+ APIKeyID :expiredAPIKey .ID ,
731+ UserID :user .ID ,
732+ HashPrefix : []byte (fmt .Sprintf ("expired-%d" ,time .Now ().UnixNano ())),
733+ })
734+
735+ // Create non-expired access token (should be retained)
736+ validToken := dbgen .OAuth2ProviderAppToken (t ,db , database.OAuth2ProviderAppToken {
737+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
738+ AppSecretID :appSecret .ID ,
739+ APIKeyID :validAPIKey .ID ,
740+ UserID :user .ID ,
741+ HashPrefix : []byte (fmt .Sprintf ("valid-%d" ,time .Now ().UnixNano ())),
742+ })
743+
744+ // Verify tokens exist initially
745+ _ ,err := db .GetOAuth2ProviderAppTokenByPrefix (ctx ,expiredToken .HashPrefix )
746+ require .NoError (t ,err )
747+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,validToken .HashPrefix )
748+ require .NoError (t ,err )
749+
750+ // Run cleanup
751+ done := awaitDoTick (ctx ,t ,clk )
752+ closer := dbpurge .New (ctx ,logger ,db ,clk )
753+ defer closer .Close ()
754+ <- done
755+
756+ // Verify expired token is deleted
757+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,expiredToken .HashPrefix )
758+ require .Error (t ,err )
759+ require .ErrorIs (t ,err ,sql .ErrNoRows )
760+
761+ // Verify non-expired token is retained
762+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,validToken .HashPrefix )
763+ require .NoError (t ,err )
764+ }
765+
766+ //nolint:paralleltest // It uses LockIDDBPurge.
767+ func TestDeleteExpiredOAuth2ProviderDeviceCodes (t * testing.T ) {
768+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
769+ defer cancel ()
770+
771+ clk := quartz .NewMock (t )
772+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
773+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
774+
775+ now := dbtime .Now ()
776+ clk .Set (now ).MustWait (ctx )
777+
778+ // Create test data
779+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
780+ Name :fmt .Sprintf ("test-device-%d" ,time .Now ().UnixNano ()),
781+ })
782+
783+ nanoTime := time .Now ().UnixNano ()
784+
785+ // Create expired device code with pending status (should be deleted)
786+ expiredPendingCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
787+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
788+ ClientID :app .ID ,
789+ Status :database .OAuth2DeviceStatusPending ,
790+ DeviceCodePrefix :fmt .Sprintf ("EP%06d" ,nanoTime % 1000000 ),
791+ UserCode :fmt .Sprintf ("EP%06d" ,nanoTime % 1000000 ),
792+ DeviceCodeHash :fmt .Appendf (nil ,"hash-exp-pending-%d" ,nanoTime ),
793+ })
794+
795+ // Create non-expired device code with pending status (should be retained)
796+ validPendingCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
797+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
798+ ClientID :app .ID ,
799+ Status :database .OAuth2DeviceStatusPending ,
800+ DeviceCodePrefix :fmt .Sprintf ("VP%06d" , (nanoTime + 1 )% 1000000 ),
801+ UserCode :fmt .Sprintf ("VP%06d" , (nanoTime + 1 )% 1000000 ),
802+ DeviceCodeHash :fmt .Appendf (nil ,"hash-val-pending-%d" ,nanoTime + 1 ),
803+ })
804+
805+ // Create expired device code with authorized status (should be deleted - all expired codes are deleted)
806+ expiredAuthorizedCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
807+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
808+ ClientID :app .ID ,
809+ DeviceCodePrefix :fmt .Sprintf ("EA%06d" , (nanoTime + 2 )% 1000000 ),
810+ UserCode :fmt .Sprintf ("EA%06d" , (nanoTime + 2 )% 1000000 ),
811+ DeviceCodeHash :fmt .Appendf (nil ,"hash-exp-auth-%d" ,nanoTime + 2 ),
812+ })
813+
814+ // Create a user and authorize the device code
815+ user := dbgen .User (t ,db , database.User {})
816+ expiredAuthorizedCode ,err := db .UpdateOAuth2ProviderDeviceCodeAuthorization (ctx , database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams {
817+ ID :expiredAuthorizedCode .ID ,
818+ UserID : uuid.NullUUID {UUID :user .ID ,Valid :true },
819+ Status :database .OAuth2DeviceStatusAuthorized ,
820+ })
821+ require .NoError (t ,err )
822+
823+ // Verify device codes exist initially
824+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredPendingCode .ID )
825+ require .NoError (t ,err )
826+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,validPendingCode .ID )
827+ require .NoError (t ,err )
828+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredAuthorizedCode .ID )
829+ require .NoError (t ,err )
830+
831+ // Run cleanup
832+ done := awaitDoTick (ctx ,t ,clk )
833+ closer := dbpurge .New (ctx ,logger ,db ,clk )
834+ defer closer .Close ()
835+ <- done
836+
837+ // Verify expired pending device code is deleted
838+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredPendingCode .ID )
839+ require .Error (t ,err )
840+ require .ErrorIs (t ,err ,sql .ErrNoRows )
841+
842+ // Verify non-expired pending device code is retained
843+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,validPendingCode .ID )
844+ require .NoError (t ,err )
845+
846+ // Verify expired authorized device code is deleted (all expired codes are deleted)
847+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredAuthorizedCode .ID )
848+ require .Error (t ,err )
849+ require .ErrorIs (t ,err ,sql .ErrNoRows )
850+ }