mirror of https://github.com/grafana/grafana.git
Folders: Update folder hook tests to use mock server handlers (#109341)
This commit is contained in:
parent
806872bfce
commit
5c542478a7
|
@ -2,8 +2,16 @@ import { HttpHandler } from 'msw';
|
|||
|
||||
import folderHandlers from './api/folders/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;
|
||||
|
|
|
@ -6,6 +6,27 @@ const [mockTree] = wellFormedTree();
|
|||
const [mockTreeThatViewersCanEdit] = treeViewersCanEdit();
|
||||
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 = () =>
|
||||
http.get('/api/folders', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
|
@ -24,6 +45,7 @@ const listFoldersHandler = () =>
|
|||
return {
|
||||
uid: folder.item.uid,
|
||||
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
|
||||
|
@ -33,10 +55,13 @@ const listFoldersHandler = () =>
|
|||
});
|
||||
|
||||
const getFolderHandler = () =>
|
||||
http.get('/api/folders/:uid', ({ params }) => {
|
||||
http.get('/api/folders/:uid', ({ params, request }) => {
|
||||
const { uid } = params;
|
||||
const url = new URL(request.url);
|
||||
const accessControlQueryParam = url.searchParams.get('accesscontrol');
|
||||
|
||||
const folder = mockTree.find((v) => v.item.uid === uid);
|
||||
|
||||
if (!folder) {
|
||||
return HttpResponse.json({ message: 'folder not found', status: 'not-found' }, { status: 404 });
|
||||
}
|
||||
|
@ -44,6 +69,8 @@ const getFolderHandler = () =>
|
|||
return HttpResponse.json({
|
||||
title: folder?.item.title,
|
||||
uid: folder?.item.uid,
|
||||
...additionalProperties,
|
||||
...(accessControlQueryParam ? { accessControl: mockAccessControl } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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()];
|
|
@ -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()];
|
|
@ -1,175 +1,107 @@
|
|||
import { QueryStatus } from '@reduxjs/toolkit/query';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { renderHook, getWrapper, waitFor } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useGetFolderQuery as useGetFolderQueryLegacy } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
|
||||
import {
|
||||
AnnoKeyCreatedBy,
|
||||
AnnoKeyFolder,
|
||||
AnnoKeyManagerKind,
|
||||
AnnoKeyUpdatedBy,
|
||||
AnnoKeyUpdatedTimestamp,
|
||||
DeprecatedInternalId,
|
||||
} from '../../../../features/apiserver/types';
|
||||
import { useGetDisplayMappingQuery } from '../../iam/v0alpha1';
|
||||
import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { getFolderFixtures } from '@grafana/test-utils/unstable';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { useGetFolderQueryFacade } from './hooks';
|
||||
|
||||
import { useGetFolderQuery, useGetFolderParentsQuery } from './index';
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
// Mocks for the hooks used inside useGetFolderQueryFacade
|
||||
jest.mock('./index', () => ({
|
||||
useGetFolderQuery: jest.fn(),
|
||||
useGetFolderParentsQuery: jest.fn(),
|
||||
}));
|
||||
const [_, { folderA, folderA_folderA }] = getFolderFixtures();
|
||||
|
||||
jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({
|
||||
useGetFolderQuery: jest.fn(),
|
||||
}));
|
||||
const expectedUid = folderA_folderA.item.uid;
|
||||
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', () => ({
|
||||
useGetDisplayMappingQuery: jest.fn(),
|
||||
}));
|
||||
const parentUrlSlug = folderA.item.title.toLowerCase().replace(/ /g, '-').replace(/[\.]/g, '');
|
||||
const expectedParentUrl = `/grafana/dashboards/f/${folderA.item.uid}/${parentUrlSlug}`;
|
||||
|
||||
// Mock config and constants
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
const runtime = jest.requireActual('@grafana/runtime');
|
||||
return {
|
||||
...runtime,
|
||||
config: {
|
||||
...runtime.config,
|
||||
featureToggles: {
|
||||
...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 renderFolderHook = async () => {
|
||||
const { result } = renderHook(() => useGetFolderQueryFacade(folderA_folderA.item.uid), {
|
||||
wrapper: getWrapper({}),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const mockParents = {
|
||||
data: { items: [{ name: 'parent-uid', title: 'Parent Folder' }] },
|
||||
...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(),
|
||||
};
|
||||
const originalToggles = { ...config.featureToggles };
|
||||
const originalAppSubUrl = String(config.appSubUrl);
|
||||
|
||||
describe('useGetFolderQueryFacade', () => {
|
||||
const oldToggleValue = config.featureToggles.foldersAppPlatformAPI;
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.foldersAppPlatformAPI = oldToggleValue;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(useGetFolderQuery as jest.Mock).mockReturnValue(mockFolder);
|
||||
(useGetFolderParentsQuery as jest.Mock).mockReturnValue(mockParents);
|
||||
(useGetDisplayMappingQuery as jest.Mock).mockReturnValue(mockUserDisplay);
|
||||
(useGetFolderQueryLegacy as jest.Mock).mockReturnValue(mockLegacyResponse);
|
||||
config.appSubUrl = '/grafana';
|
||||
});
|
||||
|
||||
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;
|
||||
const { result } = renderHook(() => useGetFolderQueryFacade('folder-uid'));
|
||||
|
||||
const result = await renderFolderHook();
|
||||
|
||||
expect(result.current.data).toMatchObject({
|
||||
canAdmin: true,
|
||||
canDelete: true,
|
||||
canEdit: true,
|
||||
canSave: true,
|
||||
created: '2023-01-01T00:00:00Z',
|
||||
createdBy: 'User Two',
|
||||
createdBy: 'User 1',
|
||||
hasAcl: false,
|
||||
id: 123,
|
||||
parentUid: 'parent-uid',
|
||||
parentUid: folderA.item.uid,
|
||||
managedBy: 'user',
|
||||
title: 'Test Folder',
|
||||
uid: 'folder-uid',
|
||||
title: expectedTitle,
|
||||
uid: expectedUid,
|
||||
updated: '2024-01-01T00:00:00Z',
|
||||
updatedBy: 'User One',
|
||||
url: '/grafana/dashboards/f/folder-uid/test-folder',
|
||||
version: 2,
|
||||
updatedBy: 'User 2',
|
||||
url: expectedUrl,
|
||||
version: 1,
|
||||
accessControl: {
|
||||
'dashboards.permissions:write': true,
|
||||
'dashboards:create': true,
|
||||
},
|
||||
parents: [
|
||||
{
|
||||
title: 'Parent Folder',
|
||||
uid: 'parent-uid',
|
||||
url: '/grafana/dashboards/f/parent-uid/parent-folder',
|
||||
title: folderA.item.title,
|
||||
uid: folderA.item.uid,
|
||||
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;
|
||||
const { result } = renderHook(() => useGetFolderQueryFacade('folder-uid'));
|
||||
expect(result.current.data).toMatchObject(mockLegacyResponse.data);
|
||||
const result = await renderFolderHook();
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue