@@ -637,4 +637,109 @@ describe('Team Members API', function () {
637637 secondResponse . statusCode . should . equal ( 200 )
638638 } )
639639 } )
640+
641+ describe ( 'PAT team scope cleanup on member removal' , function ( ) {
642+ before ( async function ( ) {
643+ if ( app ) await app . close ( )
644+ app = await setup ( )
645+ TestObjects . ATeam = await app . db . models . Team . byName ( 'ATeam' )
646+ TestObjects . BTeam = await app . db . models . Team . create ( { name : 'BTeam' , TeamTypeId : app . defaultTeamType . id } )
647+ TestObjects . CTeam = await app . db . models . Team . create ( { name : 'CTeam' , TeamTypeId : app . defaultTeamType . id } )
648+ delete TestObjects . DTeam
649+ } )
650+ after ( async function ( ) {
651+ await app . close ( )
652+ } )
653+
654+ it ( 'removing user from team deletes AccessTokenTeamScope entries for that team' , async function ( ) {
655+ await setupUsers ( )
656+ // bob is member of ATeam and BTeam. Create a PAT scoped to both.
657+ const token = await app . db . controllers . AccessToken . createPersonalAccessToken (
658+ TestObjects . bob , '' , null , 'Multi Team PAT' ,
659+ { teamIds : [ TestObjects . ATeam . hashid , TestObjects . BTeam . hashid ] }
660+ )
661+
662+ // Verify both scopes exist
663+ let scopes = await app . db . models . AccessTokenTeamScope . findAll ( {
664+ where : { UserId : TestObjects . bob . id }
665+ } )
666+ scopes . should . have . length ( 2 )
667+
668+ // Remove bob from ATeam
669+ await app . db . controllers . Team . removeUser ( TestObjects . ATeam , TestObjects . bob )
670+
671+ // ATeam scope should be gone, BTeam scope should remain
672+ scopes = await app . db . models . AccessTokenTeamScope . findAll ( {
673+ where : { UserId : TestObjects . bob . id }
674+ } )
675+ scopes . should . have . length ( 1 )
676+ scopes [ 0 ] . TeamId . should . equal ( TestObjects . BTeam . id )
677+
678+ // Token itself should still exist
679+ const tokenRecord = await app . db . models . AccessToken . findOne ( {
680+ where : { id : app . db . models . AccessToken . decodeHashid ( token . id ) }
681+ } )
682+ should ( tokenRecord ) . not . be . null ( )
683+ } )
684+
685+ it ( 'deletes token when last team scope is removed' , async function ( ) {
686+ await setupUsers ( )
687+ // bob is member of ATeam. Create a PAT scoped only to ATeam.
688+ const token = await app . db . controllers . AccessToken . createPersonalAccessToken (
689+ TestObjects . bob , '' , null , 'Single Team PAT' ,
690+ { teamIds : [ TestObjects . ATeam . hashid ] }
691+ )
692+ const tokenId = app . db . models . AccessToken . decodeHashid ( token . id )
693+
694+ // Remove bob from ATeam: last scope removed, token should be deleted
695+ await app . db . controllers . Team . removeUser ( TestObjects . ATeam , TestObjects . bob )
696+
697+ const tokenRecord = await app . db . models . AccessToken . findOne ( { where : { id : tokenId } } )
698+ should ( tokenRecord ) . be . null ( )
699+
700+ const scopes = await app . db . models . AccessTokenTeamScope . findAll ( {
701+ where : { UserId : TestObjects . bob . id }
702+ } )
703+ scopes . should . have . length ( 0 )
704+ } )
705+
706+ it ( 'team-agnostic tokens are unaffected by membership removal' , async function ( ) {
707+ await setupUsers ( )
708+ // Create a PAT with no team scopes
709+ const token = await app . db . controllers . AccessToken . createPersonalAccessToken (
710+ TestObjects . bob , '' , null , 'Global PAT'
711+ )
712+ const tokenId = app . db . models . AccessToken . decodeHashid ( token . id )
713+
714+ // Remove bob from ATeam
715+ await app . db . controllers . Team . removeUser ( TestObjects . ATeam , TestObjects . bob )
716+
717+ // Token should still exist
718+ const tokenRecord = await app . db . models . AccessToken . findOne ( { where : { id : tokenId } } )
719+ should ( tokenRecord ) . not . be . null ( )
720+ } )
721+
722+ it ( 'tokens scoped to other teams are unaffected' , async function ( ) {
723+ await setupUsers ( )
724+ // Create a PAT scoped only to BTeam
725+ const token = await app . db . controllers . AccessToken . createPersonalAccessToken (
726+ TestObjects . bob , '' , null , 'BTeam Only PAT' ,
727+ { teamIds : [ TestObjects . BTeam . hashid ] }
728+ )
729+ const tokenId = app . db . models . AccessToken . decodeHashid ( token . id )
730+
731+ // Remove bob from ATeam (not BTeam)
732+ await app . db . controllers . Team . removeUser ( TestObjects . ATeam , TestObjects . bob )
733+
734+ // Token and its BTeam scope should be untouched
735+ const tokenRecord = await app . db . models . AccessToken . findOne ( { where : { id : tokenId } } )
736+ should ( tokenRecord ) . not . be . null ( )
737+
738+ const scopes = await app . db . models . AccessTokenTeamScope . findAll ( {
739+ where : { AccessTokenId : tokenId }
740+ } )
741+ scopes . should . have . length ( 1 )
742+ scopes [ 0 ] . TeamId . should . equal ( TestObjects . BTeam . id )
743+ } )
744+ } )
640745} )
0 commit comments