feat: create app utilities for the API url resolution and permission validation

This commit is contained in:
Diana Olarte
2025-09-24 22:58:56 +10:00
committed by Adolfo R. Brandes
parent f0298dc9eb
commit 80d04ed000
3 changed files with 147 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
import { act, ReactNode } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useValidateUserPermissions } from './useValidateUserPermissions';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
return wrapper;
};
const permissions = [
{
action: 'act:read',
object: 'lib:test-lib',
scope: 'org:OpenedX',
},
];
const mockValidPermissions = [
{ action: 'act:read', object: 'lib:test-lib', allowed: true },
];
const mockInvalidPermissions = [
{ action: 'act:read', object: 'lib:test-lib', allowed: false },
];
describe('useValidateUserPermissions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('returns allowed true when permissions are valid', async () => {
getAuthenticatedHttpClient.mockReturnValue({
post: jest.fn().mockResolvedValueOnce({ data: mockValidPermissions }),
});
const { result } = renderHook(() => useValidateUserPermissions(permissions), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current).toBeDefined());
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
expect(result.current.data[0].allowed).toBe(true);
});
it('returns allowed false when permissions are invalid', async () => {
getAuthenticatedHttpClient.mockReturnValue({
post: jest.fn().mockResolvedValue({ data: mockInvalidPermissions }),
});
const { result } = renderHook(() => useValidateUserPermissions(permissions), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current).toBeDefined());
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
expect(result.current.data[0].allowed).toBe(false);
});
it('handles error when the API call fails', async () => {
const mockError = new Error('API Error');
getAuthenticatedHttpClient.mockReturnValue({
post: jest.fn().mockRejectedValue(new Error('API Error')),
});
try {
act(() => {
renderHook(() => useValidateUserPermissions(permissions), {
wrapper: createWrapper(),
});
});
} catch (error) {
expect(error).toEqual(mockError); // Check for the expected error
return;
}
});
});

View File

@@ -0,0 +1,44 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getApiUrl } from './utils';
export interface PermissionValidationRequest {
action: string;
object?: string;
scope?: string;
}
export interface PermissionValidationResponse extends PermissionValidationRequest{
allowed: boolean;
}
const validateUserPermissions = async (validations: PermissionValidationRequest[]): Promise<PermissionValidationResponse[]> => {
const { data } = await getAuthenticatedHttpClient().post(getApiUrl(`/api/authz/v1/permissions/validate/me`), validations);
return data;
};
/**
* React Query hook to validate if the current user has permissions over a certain object in the instance.
* It helps to:
* - Determine whether the current user can access certain object.
* - Provide role-based rendering logic for UI components.
*
* @param permissions - The array of objects and actions to validate.
*
* @example
* const { data } = useValidateTeamMember([{
"action": "act:read",
"object": "lib:test-lib",
"scope": "org:OpenedX"
}]);
* if (data[0].allowed) { ... }
*
*/
export const useValidateUserPermissions = (permissions: PermissionValidationRequest[]) => {
return useSuspenseQuery<PermissionValidationResponse[], Error>({
queryKey: ['validate-user-permissions', permissions],
queryFn: () => validateUserPermissions(permissions),
retry: false,
});
}

4
src/helpers/utils.ts Normal file
View File

@@ -0,0 +1,4 @@
import { getConfig } from '@edx/frontend-platform';
export const getApiUrl = (path: string) => `${getConfig().LMS_BASE_URL}${path || ''}`;
export const getStudioApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}${path || ''}`;