@@ -490,3 +490,216 @@ func containsProvisionerDaemon(daemons []database.ProvisionerDaemon, name string
490490return d .Name == name
491491})
492492}
493+
494+ //nolint:paralleltest // It uses LockIDDBPurge.
495+ func TestDeleteExpiredOAuth2ProviderAppCodes (t * testing.T ) {
496+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
497+ defer cancel ()
498+
499+ clk := quartz .NewMock (t )
500+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
501+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
502+
503+ now := dbtime .Now ()
504+ clk .Set (now ).MustWait (ctx )
505+
506+ // Create test data
507+ user := dbgen .User (t ,db , database.User {})
508+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
509+ Name :fmt .Sprintf ("test-codes-%d" ,time .Now ().UnixNano ()),
510+ })
511+
512+ // Create expired authorization code (should be deleted)
513+ expiredCode := dbgen .OAuth2ProviderAppCode (t ,db , database.OAuth2ProviderAppCode {
514+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
515+ AppID :app .ID ,
516+ UserID :user .ID ,
517+ SecretPrefix : []byte (fmt .Sprintf ("expired-%d" ,time .Now ().UnixNano ())),
518+ })
519+
520+ // Create non-expired authorization code (should be retained)
521+ validCode := dbgen .OAuth2ProviderAppCode (t ,db , database.OAuth2ProviderAppCode {
522+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
523+ AppID :app .ID ,
524+ UserID :user .ID ,
525+ SecretPrefix : []byte (fmt .Sprintf ("valid-%d" ,time .Now ().UnixNano ())),
526+ })
527+
528+ // Verify codes exist initially
529+ _ ,err := db .GetOAuth2ProviderAppCodeByID (ctx ,expiredCode .ID )
530+ require .NoError (t ,err )
531+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,validCode .ID )
532+ require .NoError (t ,err )
533+
534+ // Run cleanup
535+ done := awaitDoTick (ctx ,t ,clk )
536+ closer := dbpurge .New (ctx ,logger ,db ,clk )
537+ defer closer .Close ()
538+ <- done
539+
540+ // Verify expired code is deleted
541+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,expiredCode .ID )
542+ require .Error (t ,err )
543+ require .ErrorIs (t ,err ,sql .ErrNoRows )
544+
545+ // Verify non-expired code is retained
546+ _ ,err = db .GetOAuth2ProviderAppCodeByID (ctx ,validCode .ID )
547+ require .NoError (t ,err )
548+ }
549+
550+ //nolint:paralleltest // It uses LockIDDBPurge.
551+ func TestDeleteExpiredOAuth2ProviderAppTokens (t * testing.T ) {
552+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
553+ defer cancel ()
554+
555+ clk := quartz .NewMock (t )
556+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
557+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
558+
559+ now := dbtime .Now ()
560+ clk .Set (now ).MustWait (ctx )
561+
562+ // Create test data
563+ user := dbgen .User (t ,db , database.User {})
564+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
565+ Name :fmt .Sprintf ("test-tokens-%d" ,time .Now ().UnixNano ()),
566+ })
567+ appSecret := dbgen .OAuth2ProviderAppSecret (t ,db , database.OAuth2ProviderAppSecret {
568+ AppID :app .ID ,
569+ })
570+
571+ // Create API keys for the tokens
572+ expiredAPIKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
573+ UserID :user .ID ,
574+ ExpiresAt :now .Add (- 1 * time .Hour ),
575+ })
576+ validAPIKey ,_ := dbgen .APIKey (t ,db , database.APIKey {
577+ UserID :user .ID ,
578+ ExpiresAt :now .Add (24 * time .Hour ),// Valid for 24 hours
579+ })
580+
581+ // Create expired access token (should be deleted)
582+ expiredToken := dbgen .OAuth2ProviderAppToken (t ,db , database.OAuth2ProviderAppToken {
583+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
584+ AppSecretID :appSecret .ID ,
585+ APIKeyID :expiredAPIKey .ID ,
586+ UserID :user .ID ,
587+ HashPrefix : []byte (fmt .Sprintf ("expired-%d" ,time .Now ().UnixNano ())),
588+ })
589+
590+ // Create non-expired access token (should be retained)
591+ validToken := dbgen .OAuth2ProviderAppToken (t ,db , database.OAuth2ProviderAppToken {
592+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
593+ AppSecretID :appSecret .ID ,
594+ APIKeyID :validAPIKey .ID ,
595+ UserID :user .ID ,
596+ HashPrefix : []byte (fmt .Sprintf ("valid-%d" ,time .Now ().UnixNano ())),
597+ })
598+
599+ // Verify tokens exist initially
600+ _ ,err := db .GetOAuth2ProviderAppTokenByPrefix (ctx ,expiredToken .HashPrefix )
601+ require .NoError (t ,err )
602+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,validToken .HashPrefix )
603+ require .NoError (t ,err )
604+
605+ // Run cleanup
606+ done := awaitDoTick (ctx ,t ,clk )
607+ closer := dbpurge .New (ctx ,logger ,db ,clk )
608+ defer closer .Close ()
609+ <- done
610+
611+ // Verify expired token is deleted
612+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,expiredToken .HashPrefix )
613+ require .Error (t ,err )
614+ require .ErrorIs (t ,err ,sql .ErrNoRows )
615+
616+ // Verify non-expired token is retained
617+ _ ,err = db .GetOAuth2ProviderAppTokenByPrefix (ctx ,validToken .HashPrefix )
618+ require .NoError (t ,err )
619+ }
620+
621+ //nolint:paralleltest // It uses LockIDDBPurge.
622+ func TestDeleteExpiredOAuth2ProviderDeviceCodes (t * testing.T ) {
623+ ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitShort )
624+ defer cancel ()
625+
626+ clk := quartz .NewMock (t )
627+ db ,_ := dbtestutil .NewDB (t ,dbtestutil .WithDumpOnFailure ())
628+ logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true })
629+
630+ now := dbtime .Now ()
631+ clk .Set (now ).MustWait (ctx )
632+
633+ // Create test data
634+ app := dbgen .OAuth2ProviderApp (t ,db , database.OAuth2ProviderApp {
635+ Name :fmt .Sprintf ("test-device-%d" ,time .Now ().UnixNano ()),
636+ })
637+
638+ nanoTime := time .Now ().UnixNano ()
639+
640+ // Create expired device code with pending status (should be deleted)
641+ expiredPendingCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
642+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
643+ ClientID :app .ID ,
644+ Status :database .OAuth2DeviceStatusPending ,
645+ DeviceCodePrefix :fmt .Sprintf ("exp-pending-%d" ,nanoTime ),
646+ UserCode :fmt .Sprintf ("EP%06d" ,nanoTime % 1000000 ),
647+ DeviceCodeHash : []byte (fmt .Sprintf ("hash-exp-pending-%d" ,nanoTime )),
648+ })
649+
650+ // Create non-expired device code with pending status (should be retained)
651+ validPendingCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
652+ ExpiresAt :now .Add (1 * time .Hour ),// Expires in 1 hour
653+ ClientID :app .ID ,
654+ Status :database .OAuth2DeviceStatusPending ,
655+ DeviceCodePrefix :fmt .Sprintf ("val-pending-%d" ,nanoTime + 1 ),
656+ UserCode :fmt .Sprintf ("VP%06d" , (nanoTime + 1 )% 1000000 ),
657+ DeviceCodeHash : []byte (fmt .Sprintf ("hash-val-pending-%d" ,nanoTime + 1 )),
658+ })
659+
660+ // Create expired device code with authorized status (should be deleted - all expired codes are deleted)
661+ expiredAuthorizedCode := dbgen .OAuth2ProviderDeviceCode (t ,db , database.OAuth2ProviderDeviceCode {
662+ ExpiresAt :now .Add (- 1 * time .Hour ),// Expired 1 hour ago
663+ ClientID :app .ID ,
664+ DeviceCodePrefix :fmt .Sprintf ("exp-auth-%d" ,nanoTime + 2 ),
665+ UserCode :fmt .Sprintf ("EA%06d" , (nanoTime + 2 )% 1000000 ),
666+ DeviceCodeHash : []byte (fmt .Sprintf ("hash-exp-auth-%d" ,nanoTime + 2 )),
667+ })
668+
669+ // Create a user and authorize the device code
670+ user := dbgen .User (t ,db , database.User {})
671+ expiredAuthorizedCode ,err := db .UpdateOAuth2ProviderDeviceCodeAuthorization (ctx , database.UpdateOAuth2ProviderDeviceCodeAuthorizationParams {
672+ ID :expiredAuthorizedCode .ID ,
673+ UserID : uuid.NullUUID {UUID :user .ID ,Valid :true },
674+ Status :database .OAuth2DeviceStatusAuthorized ,
675+ })
676+ require .NoError (t ,err )
677+
678+ // Verify device codes exist initially
679+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredPendingCode .ID )
680+ require .NoError (t ,err )
681+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,validPendingCode .ID )
682+ require .NoError (t ,err )
683+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredAuthorizedCode .ID )
684+ require .NoError (t ,err )
685+
686+ // Run cleanup
687+ done := awaitDoTick (ctx ,t ,clk )
688+ closer := dbpurge .New (ctx ,logger ,db ,clk )
689+ defer closer .Close ()
690+ <- done
691+
692+ // Verify expired pending device code is deleted
693+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredPendingCode .ID )
694+ require .Error (t ,err )
695+ require .ErrorIs (t ,err ,sql .ErrNoRows )
696+
697+ // Verify non-expired pending device code is retained
698+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,validPendingCode .ID )
699+ require .NoError (t ,err )
700+
701+ // Verify expired authorized device code is deleted (all expired codes are deleted)
702+ _ ,err = db .GetOAuth2ProviderDeviceCodeByID (ctx ,expiredAuthorizedCode .ID )
703+ require .Error (t ,err )
704+ require .ErrorIs (t ,err ,sql .ErrNoRows )
705+ }