@@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
44import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth' ;
55import {
66 useLibrary , usePermissionsByRole , useTeamMembers , useAssignTeamMembersRole , useRevokeUserRoles ,
7- useValidateUsers ,
7+ useValidateUsers , useScopes , useOrganizations , useManagedScopeOrgs ,
88} from './hooks' ;
99
1010jest . mock ( '@edx/frontend-platform/auth' , ( ) => ( {
@@ -290,6 +290,183 @@ describe('useValidateUsers', () => {
290290 } ) ;
291291} ) ;
292292
293+ describe ( 'useScopes' , ( ) => {
294+ beforeEach ( ( ) => {
295+ jest . clearAllMocks ( ) ;
296+ } ) ;
297+
298+ const makeScopesResponse = ( next : string | null = null ) => ( {
299+ results : [ {
300+ id : 'lib:123' , name : 'Test Library' , org : 'testorg' , contextType : 'library' ,
301+ } ] ,
302+ count : 1 ,
303+ next,
304+ previous : null ,
305+ } ) ;
306+
307+ it ( 'returns pages data on success' , async ( ) => {
308+ getAuthenticatedHttpClient . mockReturnValue ( {
309+ get : jest . fn ( ) . mockResolvedValue ( { data : makeScopesResponse ( ) } ) ,
310+ } ) ;
311+
312+ const { result } = renderHook ( ( ) => useScopes ( { } ) , { wrapper : createWrapper ( ) } ) ;
313+
314+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
315+
316+ expect ( result . current . data ?. pages ) . toHaveLength ( 1 ) ;
317+ expect ( result . current . data ?. pages [ 0 ] . results ) . toHaveLength ( 1 ) ;
318+ } ) ;
319+
320+ it ( 'hasNextPage is false when next is null' , async ( ) => {
321+ getAuthenticatedHttpClient . mockReturnValue ( {
322+ get : jest . fn ( ) . mockResolvedValue ( { data : makeScopesResponse ( null ) } ) ,
323+ } ) ;
324+
325+ const { result } = renderHook ( ( ) => useScopes ( { } ) , { wrapper : createWrapper ( ) } ) ;
326+
327+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
328+ expect ( result . current . hasNextPage ) . toBe ( false ) ;
329+ } ) ;
330+
331+ it ( 'hasNextPage is true when next URL has page param' , async ( ) => {
332+ getAuthenticatedHttpClient . mockReturnValue ( {
333+ get : jest . fn ( ) . mockResolvedValue ( {
334+ data : makeScopesResponse ( 'http://localhost:8000/api/authz/v1/scopes/?page=2' ) ,
335+ } ) ,
336+ } ) ;
337+
338+ const { result } = renderHook ( ( ) => useScopes ( { } ) , { wrapper : createWrapper ( ) } ) ;
339+
340+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
341+ expect ( result . current . hasNextPage ) . toBe ( true ) ;
342+ } ) ;
343+
344+ it ( 'hasNextPage is false when next URL has no page param' , async ( ) => {
345+ getAuthenticatedHttpClient . mockReturnValue ( {
346+ get : jest . fn ( ) . mockResolvedValue ( {
347+ data : makeScopesResponse ( 'http://localhost:8000/api/authz/v1/scopes/' ) ,
348+ } ) ,
349+ } ) ;
350+
351+ const { result } = renderHook ( ( ) => useScopes ( { } ) , { wrapper : createWrapper ( ) } ) ;
352+
353+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
354+ expect ( result . current . hasNextPage ) . toBe ( false ) ;
355+ } ) ;
356+
357+ it ( 'hasNextPage is false when next is an invalid URL' , async ( ) => {
358+ getAuthenticatedHttpClient . mockReturnValue ( {
359+ get : jest . fn ( ) . mockResolvedValue ( {
360+ data : makeScopesResponse ( 'not-a-valid-url' ) ,
361+ } ) ,
362+ } ) ;
363+
364+ const { result } = renderHook ( ( ) => useScopes ( { } ) , { wrapper : createWrapper ( ) } ) ;
365+
366+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
367+ expect ( result . current . hasNextPage ) . toBe ( false ) ;
368+ } ) ;
369+
370+ it ( 'handles error when API call fails' , async ( ) => {
371+ getAuthenticatedHttpClient . mockReturnValue ( {
372+ get : jest . fn ( ) . mockRejectedValue ( new Error ( 'Network error' ) ) ,
373+ } ) ;
374+
375+ const { result } = renderHook ( ( ) => useScopes ( { } ) , { wrapper : createWrapper ( ) } ) ;
376+
377+ await waitFor ( ( ) => expect ( result . current . isError ) . toBe ( true ) ) ;
378+ expect ( result . current . error ) . toBeDefined ( ) ;
379+ } ) ;
380+ } ) ;
381+
382+ describe ( 'useOrganizations' , ( ) => {
383+ beforeEach ( ( ) => {
384+ jest . clearAllMocks ( ) ;
385+ } ) ;
386+
387+ it ( 'returns organizations on success' , async ( ) => {
388+ const mockOrgs = [ { org : 'org1' , name : 'Org One' } ] ;
389+ getAuthenticatedHttpClient . mockReturnValue ( {
390+ get : jest . fn ( ) . mockResolvedValue ( { data : { results : mockOrgs } } ) ,
391+ } ) ;
392+
393+ const { result } = renderHook ( ( ) => useOrganizations ( 'library' ) , { wrapper : createWrapper ( ) } ) ;
394+
395+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
396+ expect ( result . current . data ) . toEqual ( mockOrgs ) ;
397+ } ) ;
398+
399+ it ( 'handles error when API fails' , async ( ) => {
400+ getAuthenticatedHttpClient . mockReturnValue ( {
401+ get : jest . fn ( ) . mockRejectedValue ( new Error ( 'Failed' ) ) ,
402+ } ) ;
403+
404+ const { result } = renderHook ( ( ) => useOrganizations ( ) , { wrapper : createWrapper ( ) } ) ;
405+
406+ await waitFor ( ( ) => expect ( result . current . isError ) . toBe ( true ) ) ;
407+ } ) ;
408+ } ) ;
409+
410+ describe ( 'useManagedScopeOrgs' , ( ) => {
411+ beforeEach ( ( ) => {
412+ jest . clearAllMocks ( ) ;
413+ } ) ;
414+
415+ it ( 'does not fetch when contextType is undefined' , async ( ) => {
416+ const mockGet = jest . fn ( ) ;
417+ getAuthenticatedHttpClient . mockReturnValue ( { get : mockGet } ) ;
418+
419+ const { result } = renderHook ( ( ) => useManagedScopeOrgs ( undefined ) , { wrapper : createWrapper ( ) } ) ;
420+
421+ // Query is disabled, so it should not be loading or have fetched
422+ expect ( result . current . isFetching ) . toBe ( false ) ;
423+ expect ( mockGet ) . not . toHaveBeenCalled ( ) ;
424+ } ) ;
425+
426+ it ( 'fetches and returns a Set of orgs when contextType is provided' , async ( ) => {
427+ const mockScopesResponse = {
428+ results : [
429+ {
430+ id : 'lib:123' , name : 'Lib 1' , org : 'org1' , contextType : 'library' ,
431+ } ,
432+ {
433+ id : 'lib:456' , name : 'Lib 2' , org : 'org2' , contextType : 'library' ,
434+ } ,
435+ {
436+ id : 'lib:789' , name : 'Lib 3' , org : '' , contextType : 'library' ,
437+ } ,
438+ ] ,
439+ count : 3 ,
440+ next : null ,
441+ previous : null ,
442+ } ;
443+ getAuthenticatedHttpClient . mockReturnValue ( {
444+ get : jest . fn ( ) . mockResolvedValue ( { data : mockScopesResponse } ) ,
445+ } ) ;
446+
447+ const { result } = renderHook ( ( ) => useManagedScopeOrgs ( 'library' ) , { wrapper : createWrapper ( ) } ) ;
448+
449+ await waitFor ( ( ) => expect ( result . current . isSuccess ) . toBe ( true ) ) ;
450+
451+ const orgs = result . current . data as Set < string > ;
452+ expect ( orgs . has ( 'org1' ) ) . toBe ( true ) ;
453+ expect ( orgs . has ( 'org2' ) ) . toBe ( true ) ;
454+ // empty string org is filtered out
455+ expect ( orgs . has ( '' ) ) . toBe ( false ) ;
456+ expect ( orgs . size ) . toBe ( 2 ) ;
457+ } ) ;
458+
459+ it ( 'handles error when API fails' , async ( ) => {
460+ getAuthenticatedHttpClient . mockReturnValue ( {
461+ get : jest . fn ( ) . mockRejectedValue ( new Error ( 'API error' ) ) ,
462+ } ) ;
463+
464+ const { result } = renderHook ( ( ) => useManagedScopeOrgs ( 'course' ) , { wrapper : createWrapper ( ) } ) ;
465+
466+ await waitFor ( ( ) => expect ( result . current . isError ) . toBe ( true ) ) ;
467+ } ) ;
468+ } ) ;
469+
293470describe ( 'useRevokeUserRoles' , ( ) => {
294471 beforeEach ( ( ) => {
295472 jest . clearAllMocks ( ) ;
0 commit comments