Folders: Update folder hook tests to use mock server handlers (#109341)

This commit is contained in:
Tom Ratcliffe 2025-08-21 12:58:49 +01:00 committed by GitHub
parent 806872bfce
commit 5c542478a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 251 additions and 138 deletions

View File

@ -2,8 +2,16 @@ import { HttpHandler } from 'msw';
import folderHandlers from './api/folders/handlers'; import folderHandlers from './api/folders/handlers';
import teamsHandlers from './api/teams/handlers'; import teamsHandlers from './api/teams/handlers';
import appPlatformFolderHandlers from './apis/dashboard.grafana.app/v0alpha1/handlers'; import appPlatformDashboardv0alpha1Handlers from './apis/dashboard.grafana.app/v0alpha1/handlers';
import appPlatformFolderv1beta1Handlers from './apis/folder.grafana.app/v1beta1/handlers';
import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/handlers';
const allHandlers: HttpHandler[] = [...teamsHandlers, ...folderHandlers, ...appPlatformFolderHandlers]; const allHandlers: HttpHandler[] = [
...teamsHandlers,
...folderHandlers,
...appPlatformDashboardv0alpha1Handlers,
...appPlatformFolderv1beta1Handlers,
...appPlatformIamv0alpha1Handlers,
];
export default allHandlers; export default allHandlers;

View File

@ -6,6 +6,27 @@ const [mockTree] = wellFormedTree();
const [mockTreeThatViewersCanEdit] = treeViewersCanEdit(); const [mockTreeThatViewersCanEdit] = treeViewersCanEdit();
const collator = new Intl.Collator(); const collator = new Intl.Collator();
// TODO: Generalise access control response and additional properties
const mockAccessControl = {
'dashboards.permissions:write': true,
'dashboards:create': true,
};
const additionalProperties = {
canAdmin: true,
canDelete: true,
canEdit: true,
canSave: true,
created: '2025-07-14T12:07:36+02:00',
createdBy: 'Anonymous',
hasAcl: false,
id: 1,
orgId: 1,
updated: '2025-07-15T18:01:36+02:00',
updatedBy: 'Anonymous',
url: '/grafana/dashboards/f/1ca93012-1ffc-5d64-ae2e-54835c234c67/rik-cujahda-pi',
version: 1,
};
const listFoldersHandler = () => const listFoldersHandler = () =>
http.get('/api/folders', ({ request }) => { http.get('/api/folders', ({ request }) => {
const url = new URL(request.url); const url = new URL(request.url);
@ -24,6 +45,7 @@ const listFoldersHandler = () =>
return { return {
uid: folder.item.uid, uid: folder.item.uid,
title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen", title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen",
...additionalProperties,
}; };
}) })
.sort((a, b) => collator.compare(a.title, b.title)) // API always sorts by title .sort((a, b) => collator.compare(a.title, b.title)) // API always sorts by title
@ -33,10 +55,13 @@ const listFoldersHandler = () =>
}); });
const getFolderHandler = () => const getFolderHandler = () =>
http.get('/api/folders/:uid', ({ params }) => { http.get('/api/folders/:uid', ({ params, request }) => {
const { uid } = params; const { uid } = params;
const url = new URL(request.url);
const accessControlQueryParam = url.searchParams.get('accesscontrol');
const folder = mockTree.find((v) => v.item.uid === uid); const folder = mockTree.find((v) => v.item.uid === uid);
if (!folder) { if (!folder) {
return HttpResponse.json({ message: 'folder not found', status: 'not-found' }, { status: 404 }); return HttpResponse.json({ message: 'folder not found', status: 'not-found' }, { status: 404 });
} }
@ -44,6 +69,8 @@ const getFolderHandler = () =>
return HttpResponse.json({ return HttpResponse.json({
title: folder?.item.title, title: folder?.item.title,
uid: folder?.item.uid, uid: folder?.item.uid,
...additionalProperties,
...(accessControlQueryParam ? { accessControl: mockAccessControl } : {}),
}); });
}); });

View File

@ -0,0 +1,117 @@
import { HttpResponse, http } from 'msw';
import { wellFormedTree } from '../../../../fixtures/folders';
const [mockTree] = wellFormedTree();
const getFolderHandler = () =>
http.get<{ folderUid: string; namespace: string }>(
'/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders/:folderUid',
({ params }) => {
const { folderUid, namespace } = params;
const response = mockTree.find(({ item }) => {
return item.uid === folderUid;
});
if (!response) {
return HttpResponse.json(
{
kind: 'Status',
apiVersion: 'v1',
metadata: {},
status: 'Failure',
message: 'folder not found',
code: 404,
},
{ status: 404 }
);
}
return HttpResponse.json({
kind: 'Folder',
apiVersion: 'folder.grafana.app/v1beta1',
metadata: {
name: response.item.uid,
namespace,
uid: response.item.uid,
creationTimestamp: '2023-01-01T00:00:00Z',
annotations: {
// TODO: Generalise annotations in fixture data
'grafana.app/createdBy': 'user:1',
'grafana.app/updatedBy': 'user:2',
'grafana.app/managedBy': 'user',
'grafana.app/updatedTimestamp': '2024-01-01T00:00:00Z',
'grafana.app/folder': response.item.kind === 'folder' ? response.item.parentUID : undefined,
},
labels: {
'grafana.app/deprecatedInternalID': '123',
},
},
spec: { title: response.item.title, description: '' },
status: {},
});
}
);
const getFolderParentsHandler = () =>
http.get<{ folderUid: string; namespace: string }>(
'/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders/:folderUid/parents',
({ params }) => {
const { folderUid } = params;
const folder = mockTree.find(({ item }) => {
return item.kind === 'folder' && item.uid === folderUid;
});
if (!folder || folder.item.kind !== 'folder') {
return HttpResponse.json({
kind: 'Status',
apiVersion: 'v1',
metadata: {},
status: 'Failure',
message: 'folder not found',
code: 404,
});
}
const findParents = (parents: Array<(typeof mockTree)[number]>, folderUid?: string) => {
if (!folderUid) {
return parents;
}
const parent = mockTree.find(({ item }) => {
return item.kind === 'folder' && item.uid === folderUid;
});
if (parent) {
parents.push(parent);
return findParents(parents, parent.item.kind === 'folder' ? parent.item.parentUID : undefined);
}
return parents;
};
const parents = findParents([], folder?.item?.parentUID);
const mapped = parents.map((parent) => ({
name: parent.item.uid,
title: parent.item.title,
parentUid: parent.item.kind === 'folder' ? parent.item.parentUID : undefined,
}));
if (folder) {
mapped.push({
name: folder.item.uid,
title: folder.item.title,
parentUid: folder.item.parentUID,
});
}
return HttpResponse.json({
kind: 'FolderInfoList',
apiVersion: 'folder.grafana.app/v1beta1',
metadata: {},
items: mapped,
});
}
);
export default [getFolderHandler(), getFolderParentsHandler()];

View File

@ -0,0 +1,29 @@
import { HttpResponse, http } from 'msw';
const getDisplayMapping = () =>
http.get<{ namespace: string }>('/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/display', ({ request }) => {
const url = new URL(request.url);
const keys = url.searchParams.getAll('key');
// Turn query params such as `user:1` into mock mapping of `User 1` etc.
const mockMappings = keys.map((key) => {
const [_, id] = key.split(':');
const displayName = `User ${id}`;
return {
identity: {
type: 'user',
name: `u00000000${id}`,
},
displayName,
internalId: parseInt(id, 10),
};
});
return HttpResponse.json({
metadata: {},
keys,
display: mockMappings,
});
});
export default [getDisplayMapping()];

View File

@ -1,175 +1,107 @@
import { QueryStatus } from '@reduxjs/toolkit/query'; import { renderHook, getWrapper, waitFor } from 'test/test-utils';
import { renderHook } from '@testing-library/react';
import { config } from '@grafana/runtime'; import { config, setBackendSrv } from '@grafana/runtime';
import { useGetFolderQuery as useGetFolderQueryLegacy } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { setupMockServer } from '@grafana/test-utils/server';
import { getFolderFixtures } from '@grafana/test-utils/unstable';
import { import { backendSrv } from 'app/core/services/backend_srv';
AnnoKeyCreatedBy,
AnnoKeyFolder,
AnnoKeyManagerKind,
AnnoKeyUpdatedBy,
AnnoKeyUpdatedTimestamp,
DeprecatedInternalId,
} from '../../../../features/apiserver/types';
import { useGetDisplayMappingQuery } from '../../iam/v0alpha1';
import { useGetFolderQueryFacade } from './hooks'; import { useGetFolderQueryFacade } from './hooks';
import { useGetFolderQuery, useGetFolderParentsQuery } from './index'; setBackendSrv(backendSrv);
setupMockServer();
// Mocks for the hooks used inside useGetFolderQueryFacade const [_, { folderA, folderA_folderA }] = getFolderFixtures();
jest.mock('./index', () => ({
useGetFolderQuery: jest.fn(),
useGetFolderParentsQuery: jest.fn(),
}));
jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({ const expectedUid = folderA_folderA.item.uid;
useGetFolderQuery: jest.fn(), const expectedTitle = folderA_folderA.item.title;
})); const urlSlug = expectedTitle.toLowerCase().replace(/ /g, '-').replace(/[\.]/g, '');
const expectedUrl = `/grafana/dashboards/f/${expectedUid}/${urlSlug}`;
jest.mock('../../iam/v0alpha1', () => ({ const parentUrlSlug = folderA.item.title.toLowerCase().replace(/ /g, '-').replace(/[\.]/g, '');
useGetDisplayMappingQuery: jest.fn(), const expectedParentUrl = `/grafana/dashboards/f/${folderA.item.uid}/${parentUrlSlug}`;
}));
// Mock config and constants const renderFolderHook = async () => {
jest.mock('@grafana/runtime', () => { const { result } = renderHook(() => useGetFolderQueryFacade(folderA_folderA.item.uid), {
const runtime = jest.requireActual('@grafana/runtime'); wrapper: getWrapper({}),
return { });
...runtime, await waitFor(() => {
config: { expect(result.current.isLoading).toBe(false);
...runtime.config, });
featureToggles: { return result;
...runtime.config.featureToggles,
foldersAppPlatformAPI: true,
},
appSubUrl: '/grafana',
},
};
});
const mockFolder = {
data: {
metadata: {
name: 'folder-uid',
labels: { [DeprecatedInternalId]: '123' },
annotations: {
[AnnoKeyUpdatedBy]: 'user-1',
[AnnoKeyCreatedBy]: 'user-2',
[AnnoKeyFolder]: 'parent-uid',
[AnnoKeyManagerKind]: 'user',
[AnnoKeyUpdatedTimestamp]: '2024-01-01T00:00:00Z',
},
creationTimestamp: '2023-01-01T00:00:00Z',
generation: 2,
},
spec: { title: 'Test Folder' },
},
...getResponseAttributes(),
}; };
const mockParents = { const originalToggles = { ...config.featureToggles };
data: { items: [{ name: 'parent-uid', title: 'Parent Folder' }] }, const originalAppSubUrl = String(config.appSubUrl);
...getResponseAttributes(),
};
const mockLegacyResponse = {
data: {
id: 1,
uid: 'uiduiduid',
orgId: 1,
title: 'bar',
url: '/dashboards/f/uiduiduid/bar',
hasAcl: false,
canSave: true,
canEdit: true,
canAdmin: true,
canDelete: true,
createdBy: 'Anonymous',
created: '2025-07-14T12:07:36+02:00',
updatedBy: 'Anonymous',
updated: '2025-07-15T18:01:36+02:00',
version: 1,
accessControl: {
'dashboards.permissions:write': true,
'dashboards:create': true,
},
},
...getResponseAttributes(),
};
const mockUserDisplay = {
data: {
keys: ['user-1', 'user-2'],
display: [{ displayName: 'User One' }, { displayName: 'User Two' }],
},
...getResponseAttributes(),
};
describe('useGetFolderQueryFacade', () => { describe('useGetFolderQueryFacade', () => {
const oldToggleValue = config.featureToggles.foldersAppPlatformAPI;
afterAll(() => {
config.featureToggles.foldersAppPlatformAPI = oldToggleValue;
});
beforeEach(() => { beforeEach(() => {
(useGetFolderQuery as jest.Mock).mockReturnValue(mockFolder); config.appSubUrl = '/grafana';
(useGetFolderParentsQuery as jest.Mock).mockReturnValue(mockParents);
(useGetDisplayMappingQuery as jest.Mock).mockReturnValue(mockUserDisplay);
(useGetFolderQueryLegacy as jest.Mock).mockReturnValue(mockLegacyResponse);
}); });
it('merges multiple responses into a single FolderDTO-like object if flag is true', () => { afterEach(() => {
config.featureToggles = originalToggles;
config.appSubUrl = originalAppSubUrl;
});
it('merges multiple responses into a single FolderDTO-like object if flag is true', async () => {
config.featureToggles.foldersAppPlatformAPI = true; config.featureToggles.foldersAppPlatformAPI = true;
const { result } = renderHook(() => useGetFolderQueryFacade('folder-uid'));
const result = await renderFolderHook();
expect(result.current.data).toMatchObject({ expect(result.current.data).toMatchObject({
canAdmin: true, canAdmin: true,
canDelete: true, canDelete: true,
canEdit: true, canEdit: true,
canSave: true, canSave: true,
created: '2023-01-01T00:00:00Z', created: '2023-01-01T00:00:00Z',
createdBy: 'User Two', createdBy: 'User 1',
hasAcl: false, hasAcl: false,
id: 123, id: 123,
parentUid: 'parent-uid', parentUid: folderA.item.uid,
managedBy: 'user', managedBy: 'user',
title: 'Test Folder', title: expectedTitle,
uid: 'folder-uid', uid: expectedUid,
updated: '2024-01-01T00:00:00Z', updated: '2024-01-01T00:00:00Z',
updatedBy: 'User One', updatedBy: 'User 2',
url: '/grafana/dashboards/f/folder-uid/test-folder', url: expectedUrl,
version: 2, version: 1,
accessControl: { accessControl: {
'dashboards.permissions:write': true, 'dashboards.permissions:write': true,
'dashboards:create': true, 'dashboards:create': true,
}, },
parents: [ parents: [
{ {
title: 'Parent Folder', title: folderA.item.title,
uid: 'parent-uid', uid: folderA.item.uid,
url: '/grafana/dashboards/f/parent-uid/parent-folder', url: expectedParentUrl,
}, },
], ],
}); });
}); });
it('returns legacy folder response if flag is false', () => { it('returns legacy folder response if flag is false', async () => {
config.featureToggles.foldersAppPlatformAPI = false; config.featureToggles.foldersAppPlatformAPI = false;
const { result } = renderHook(() => useGetFolderQueryFacade('folder-uid')); const result = await renderFolderHook();
expect(result.current.data).toMatchObject(mockLegacyResponse.data); expect(result.current.data).toMatchObject({
id: 1,
title: folderA_folderA.item.title,
url: expectedUrl,
uid: expectedUid,
orgId: 1,
hasAcl: false,
canSave: true,
canEdit: true,
canAdmin: true,
canDelete: true,
createdBy: 'Anonymous',
created: '2025-07-14T12:07:36+02:00',
updatedBy: 'Anonymous',
updated: '2025-07-15T18:01:36+02:00',
version: 1,
accessControl: {
'dashboards.permissions:write': true,
'dashboards:create': true,
},
});
}); });
}); });
function getResponseAttributes() {
return {
status: QueryStatus.fulfilled,
isUninitialized: false,
isLoading: false,
isFetching: false,
isSuccess: true,
isError: false,
error: undefined,
refetch: jest.fn(),
};
}