1- import { describe , it , expect , vi , beforeEach } from 'vitest' ;
1+ import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
22
33// Build a mock Octokit instance with spies for the methods we use.
44const mockChecksCreate = vi . fn ( ) . mockResolvedValue ( { data : { id : 42 } } ) ;
@@ -11,6 +11,7 @@ const mockListPRsForCommit = vi.fn().mockResolvedValue({ data: [] });
1111const mockCompareCommits = vi . fn ( ) . mockResolvedValue ( { data : { merge_base_commit : { sha : 'merge-base-sha' } } } ) ;
1212const mockPullsGet = vi . fn ( ) . mockResolvedValue ( { data : { number : 7 , head : { sha : 'abc' , ref : 'feat' } , base : { sha : 'base' , ref : 'main' } , labels : [ ] } } ) ;
1313const mockIssuesCreateComment = vi . fn ( ) . mockResolvedValue ( { } ) ;
14+ const mockListMembersInOrg = vi . fn ( ) . mockResolvedValue ( { data : [ { login : 'alice' } , { login : 'bob' } ] } ) ;
1415
1516const mockOctokit = {
1617 checks : {
@@ -31,6 +32,9 @@ const mockOctokit = {
3132 listPullRequestsAssociatedWithCommit : mockListPRsForCommit ,
3233 compareCommits : mockCompareCommits ,
3334 } ,
35+ teams : {
36+ listMembersInOrg : mockListMembersInOrg ,
37+ } ,
3438} ;
3539
3640vi . mock ( '../auth.js' , ( ) => ( {
@@ -41,6 +45,7 @@ const {
4145 createCheckRun, startCheckRun, completeCheckRun,
4246 skipCheckRun, findPullRequestBySha, getMergeBaseSha,
4347 ensureLabelsExist, setLabels, getPullRequest, createPrComment,
48+ getTeamMembers, clearTeamMemberCache,
4449} = await import ( '../github.js' ) ;
4550
4651const BASE = {
@@ -368,3 +373,81 @@ describe('createPrComment()', () => {
368373 } ) ) ;
369374 } ) ;
370375} ) ;
376+
377+ describe ( 'getTeamMembers()' , ( ) => {
378+ beforeEach ( ( ) => {
379+ vi . clearAllMocks ( ) ;
380+ clearTeamMemberCache ( ) ;
381+ } ) ;
382+
383+ afterEach ( ( ) => {
384+ vi . useRealTimers ( ) ;
385+ } ) ;
386+
387+ it ( 'returns empty array when no team slugs are provided' , async ( ) => {
388+ const result = await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ ] } ) ;
389+ expect ( result ) . toEqual ( [ ] ) ;
390+ expect ( mockListMembersInOrg ) . not . toHaveBeenCalled ( ) ;
391+ } ) ;
392+
393+ it ( 'calls the API and returns members for each slug' , async ( ) => {
394+ const result = await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'security-team' ] } ) ;
395+
396+ expect ( mockListMembersInOrg ) . toHaveBeenCalledOnce ( ) ;
397+ expect ( mockListMembersInOrg ) . toHaveBeenCalledWith ( { org : 'org' , team_slug : 'security-team' } ) ;
398+ expect ( result ) . toEqual ( [ 'alice' , 'bob' ] ) ;
399+ } ) ;
400+
401+ it ( 'uses the cached result on a second call within TTL' , async ( ) => {
402+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'security-team' ] } ) ;
403+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'security-team' ] } ) ;
404+
405+ expect ( mockListMembersInOrg ) . toHaveBeenCalledOnce ( ) ;
406+ } ) ;
407+
408+ it ( 're-fetches after the TTL expires' , async ( ) => {
409+ vi . useFakeTimers ( ) ;
410+
411+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'security-team' ] } ) ;
412+ vi . advanceTimersByTime ( 31 * 60 * 1000 ) ; // past 30-minute TTL
413+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'security-team' ] } ) ;
414+
415+ expect ( mockListMembersInOrg ) . toHaveBeenCalledTimes ( 2 ) ;
416+ } ) ;
417+
418+ it ( 'caches slugs independently so shared teams are not double-fetched' , async ( ) => {
419+ mockListMembersInOrg
420+ . mockResolvedValueOnce ( { data : [ { login : 'alice' } ] } )
421+ . mockResolvedValueOnce ( { data : [ { login : 'carol' } ] } ) ;
422+
423+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'team-a' , 'team-b' ] } ) ;
424+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'team-a' ] } ) ;
425+
426+ // team-a and team-b fetched once each on first call; second call hits cache
427+ expect ( mockListMembersInOrg ) . toHaveBeenCalledTimes ( 2 ) ;
428+ } ) ;
429+
430+ it ( 'scopes the cache by installationId' , async ( ) => {
431+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'security-team' ] } ) ;
432+ await getTeamMembers ( { installationId : 2 , org : 'org' , teamSlugs : [ 'security-team' ] } ) ;
433+
434+ expect ( mockListMembersInOrg ) . toHaveBeenCalledTimes ( 2 ) ;
435+ } ) ;
436+
437+ it ( 'parses org/slug format and uses the explicit org' , async ( ) => {
438+ await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'other-org/security-team' ] } ) ;
439+
440+ expect ( mockListMembersInOrg ) . toHaveBeenCalledWith ( { org : 'other-org' , team_slug : 'security-team' } ) ;
441+ } ) ;
442+
443+ it ( 'logs and continues when the API call fails for a slug' , async ( ) => {
444+ mockListMembersInOrg . mockRejectedValueOnce ( new Error ( 'API error' ) ) ;
445+ const error = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
446+
447+ const result = await getTeamMembers ( { installationId : 1 , org : 'org' , teamSlugs : [ 'bad-team' ] } ) ;
448+
449+ expect ( result ) . toEqual ( [ ] ) ;
450+ expect ( error ) . toHaveBeenCalledWith ( expect . stringContaining ( '[github]' ) ) ;
451+ error . mockRestore ( ) ;
452+ } ) ;
453+ } ) ;
0 commit comments