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 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;
|
||||||
|
|
|
@ -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 } : {}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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, 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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue