Skip to main content
Glama
dashboard.test.ts20.2 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DashboardService } from './dashboard.js'; import { GrafanaHttpClient } from '../http-client.js'; import type { Dashboard, DashboardDetail, Panel } from '../types.js'; // Mock the http client const mockHttpClient = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), } as unknown as GrafanaHttpClient; describe('DashboardService', () => { let service: DashboardService; beforeEach(() => { vi.clearAllMocks(); service = new DashboardService(mockHttpClient); }); describe('constructor', () => { it('should create service with correct name and version', () => { expect(service.name).toBe('DashboardService'); expect(service.version).toBe('1.0.0'); }); }); describe('searchDashboards', () => { const mockDashboards: Dashboard[] = [ { uid: 'dash1', title: 'Dashboard 1', tags: ['monitoring'], uri: 'db/dashboard-1', url: '/d/dash1/dashboard-1', type: 'dash-db', }, { uid: 'dash2', title: 'Dashboard 2', tags: ['alerts'], uri: 'db/dashboard-2', url: '/d/dash2/dashboard-2', type: 'dash-db', }, ]; beforeEach(() => { vi.mocked(mockHttpClient.get).mockResolvedValue(mockDashboards); }); it('should search dashboards with default options', async () => { const result = await service.searchDashboards(); expect(result).toEqual(mockDashboards); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000 }); }); it('should search dashboards with query parameter', async () => { await service.searchDashboards({ query: 'monitoring' }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000, query: 'monitoring', }); }); it('should search dashboards with tags', async () => { await service.searchDashboards({ tags: ['monitoring', 'prod'] }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000, tag: ['monitoring', 'prod'], }); }); it('should search dashboards with starred filter', async () => { await service.searchDashboards({ starred: true }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000, starred: true, }); }); it('should search dashboards with folderId', async () => { await service.searchDashboards({ folderId: 42 }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000, folderId: 42, }); }); it('should search dashboards with type filter', async () => { await service.searchDashboards({ type: 'dash-folder' }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000, type: 'dash-folder', }); }); it('should search dashboards with custom limit', async () => { await service.searchDashboards({ limit: 50 }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 50, }); }); it('should search dashboards with multiple parameters', async () => { await service.searchDashboards({ query: 'test', tags: ['tag1'], starred: false, folderId: 1, type: 'dash-db', limit: 100, }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { query: 'test', tag: ['tag1'], starred: false, folderId: 1, type: 'dash-db', limit: 100, }); }); it('should handle empty tags array', async () => { await service.searchDashboards({ tags: [] }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000, }); }); it('should handle zero folderId', async () => { await service.searchDashboards({ folderId: 0 }); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/search', { limit: 1000, folderId: 0, }); }); it('should not include starred parameter when undefined', async () => { await service.searchDashboards({ starred: undefined }); const calledParams = vi.mocked(mockHttpClient.get).mock.calls[0][1]; expect(calledParams).not.toHaveProperty('starred'); }); }); describe('searchDashboardsResult', () => { it('should return result wrapper', async () => { const mockDashboards: Dashboard[] = [ { uid: 'dash1', title: 'Dashboard 1', tags: ['monitoring'], uri: 'db/dashboard-1', url: '/d/dash1/dashboard-1', type: 'dash-db', }, ]; vi.mocked(mockHttpClient.get).mockResolvedValue(mockDashboards); const result = await service.searchDashboardsResult({ query: 'test' }); expect(result.success).toBe(true); expect(result.data).toEqual(mockDashboards); expect(result.error).toBeUndefined(); }); it('should handle errors in result wrapper', async () => { const error = new Error('Search failed'); vi.mocked(mockHttpClient.get).mockRejectedValue(error); const result = await service.searchDashboardsResult({ query: 'test' }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(result.data).toBeUndefined(); }); }); describe('getDashboardByUid', () => { it('should get dashboard by UID', async () => { const mockDashboardDetail: DashboardDetail = { dashboard: { id: 1, uid: 'test-uid', title: 'Test Dashboard', tags: ['test'], timezone: 'UTC', panels: [], time: { from: 'now-1h', to: 'now' }, timepicker: {}, templating: { list: [] }, annotations: { list: [] }, refresh: '5s', schemaVersion: 30, version: 1, links: [], }, meta: { type: 'db', canSave: true, canEdit: true, canAdmin: true, canStar: true, canDelete: true, slug: 'test-dashboard', url: '/d/test-uid/test-dashboard', expires: '0001-01-01T00:00:00Z', created: '2023-01-01T00:00:00Z', updated: '2023-01-02T00:00:00Z', updatedBy: 'admin', createdBy: 'admin', version: 1, hasAcl: false, isFolder: false, folderId: 0, folderUid: '', folderTitle: '', folderUrl: '', provisioned: false, provisionedExternalId: '', }, }; vi.mocked(mockHttpClient.get).mockResolvedValue(mockDashboardDetail); const result = await service.getDashboardByUid('test-uid'); expect(result).toEqual(mockDashboardDetail); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/dashboards/uid/test-uid'); }); }); describe('updateDashboard', () => { it('should update dashboard with minimal options', async () => { const mockResponse = { id: 1, uid: 'test-uid', version: 2 }; const dashboard = { uid: 'test-uid', title: 'Updated Dashboard' }; vi.mocked(mockHttpClient.post).mockResolvedValue(mockResponse); const result = await service.updateDashboard({ dashboard }); expect(result).toEqual(mockResponse); expect(mockHttpClient.post).toHaveBeenCalledWith('/api/dashboards/db', { dashboard, overwrite: false, }); }); it('should update dashboard with all options', async () => { const mockResponse = { id: 1, uid: 'test-uid', version: 2 }; const dashboard = { uid: 'test-uid', title: 'Updated Dashboard' }; vi.mocked(mockHttpClient.post).mockResolvedValue(mockResponse); const result = await service.updateDashboard({ dashboard, folderId: 42, message: 'Update message', overwrite: true, }); expect(result).toEqual(mockResponse); expect(mockHttpClient.post).toHaveBeenCalledWith('/api/dashboards/db', { dashboard, folderId: 42, message: 'Update message', overwrite: true, }); }); it('should handle zero folderId', async () => { const mockResponse = { id: 1, uid: 'test-uid', version: 2 }; const dashboard = { uid: 'test-uid', title: 'Updated Dashboard' }; vi.mocked(mockHttpClient.post).mockResolvedValue(mockResponse); await service.updateDashboard({ dashboard, folderId: 0, }); expect(mockHttpClient.post).toHaveBeenCalledWith('/api/dashboards/db', { dashboard, folderId: 0, overwrite: false, }); }); it('should not include folderId when undefined', async () => { const dashboard = { uid: 'test-uid', title: 'Updated Dashboard' }; vi.mocked(mockHttpClient.post).mockResolvedValue({}); await service.updateDashboard({ dashboard, folderId: undefined, message: 'test', }); const calledPayload = vi.mocked(mockHttpClient.post).mock.calls[0][1]; expect(calledPayload).not.toHaveProperty('folderId'); expect(calledPayload).toHaveProperty('message', 'test'); }); }); describe('getDashboardPanelQueries', () => { it('should extract panel queries from dashboard', async () => { const mockDashboardDetail: DashboardDetail = { dashboard: { id: 1, uid: 'test-uid', title: 'Test Dashboard', tags: [], timezone: 'UTC', panels: [ { id: 1, title: 'Panel 1', type: 'graph', targets: [ { refId: 'A', expr: 'up', queryType: 'prometheus', datasource: { uid: 'prom-uid', type: 'prometheus' }, }, { refId: 'B', logQL: '{job="app"}', queryType: 'loki', datasource: { uid: 'loki-uid', type: 'loki' }, }, ], datasource: { uid: 'default-uid', type: 'prometheus' }, } as Panel, { id: 2, title: 'Panel 2', type: 'stat', targets: [], datasource: { uid: 'stat-uid', type: 'stat' }, } as Panel, ], time: { from: 'now-1h', to: 'now' }, timepicker: {}, templating: { list: [] }, annotations: { list: [] }, refresh: '5s', schemaVersion: 30, version: 1, links: [], }, meta: { type: 'db', canSave: true, canEdit: true, canAdmin: true, canStar: true, canDelete: true, slug: 'test-dashboard', url: '/d/test-uid/test-dashboard', expires: '0001-01-01T00:00:00Z', created: '2023-01-01T00:00:00Z', updated: '2023-01-02T00:00:00Z', updatedBy: 'admin', createdBy: 'admin', version: 1, hasAcl: false, isFolder: false, folderId: 0, folderUid: '', folderTitle: '', folderUrl: '', provisioned: false, provisionedExternalId: '', }, }; vi.mocked(mockHttpClient.get).mockResolvedValue(mockDashboardDetail); const result = await service.getDashboardPanelQueries('test-uid'); expect(result).toEqual({ uid: 'test-uid', title: 'Test Dashboard', panels: [ { id: 1, title: 'Panel 1', type: 'graph', queries: [ { refId: 'A', query: 'up', queryType: 'prometheus', datasource: { uid: 'prom-uid', type: 'prometheus' }, }, { refId: 'B', query: '{job="app"}', queryType: 'loki', datasource: { uid: 'loki-uid', type: 'loki' }, }, ], datasource: { uid: 'default-uid', type: 'prometheus' }, }, { id: 2, title: 'Panel 2', type: 'stat', queries: [], datasource: { uid: 'stat-uid', type: 'stat' }, }, ], }); }); it('should handle panels without targets', async () => { const mockDashboardDetail: DashboardDetail = { dashboard: { id: 1, uid: 'test-uid', title: 'Test Dashboard', tags: [], timezone: 'UTC', panels: [ { id: 1, title: 'Panel 1', type: 'text', // No targets property } as Panel, ], time: { from: 'now-1h', to: 'now' }, timepicker: {}, templating: { list: [] }, annotations: { list: [] }, refresh: '5s', schemaVersion: 30, version: 1, links: [], }, meta: {} as any, }; vi.mocked(mockHttpClient.get).mockResolvedValue(mockDashboardDetail); const result = await service.getDashboardPanelQueries('test-uid'); expect(result.panels[0].queries).toEqual([]); }); it('should handle targets without expr or logQL', async () => { const mockDashboardDetail: DashboardDetail = { dashboard: { id: 1, uid: 'test-uid', title: 'Test Dashboard', tags: [], timezone: 'UTC', panels: [ { id: 1, title: 'Panel 1', type: 'graph', targets: [ { refId: 'A', // No expr or logQL queryType: 'custom', }, ], } as Panel, ], time: { from: 'now-1h', to: 'now' }, timepicker: {}, templating: { list: [] }, annotations: { list: [] }, refresh: '5s', schemaVersion: 30, version: 1, links: [], }, meta: {} as any, }; vi.mocked(mockHttpClient.get).mockResolvedValue(mockDashboardDetail); const result = await service.getDashboardPanelQueries('test-uid'); expect(result.panels[0].queries[0].query).toBe(''); }); }); describe('deleteDashboard', () => { it('should delete dashboard by UID', async () => { vi.mocked(mockHttpClient.delete).mockResolvedValue(undefined); await service.deleteDashboard('test-uid'); expect(mockHttpClient.delete).toHaveBeenCalledWith('/api/dashboards/uid/test-uid'); }); }); describe('getDashboardPermissions', () => { it('should get dashboard permissions', async () => { const mockPermissions = [ { id: 1, userId: 1, permission: 1 }, { id: 2, teamId: 1, permission: 2 }, ]; vi.mocked(mockHttpClient.get).mockResolvedValue(mockPermissions); const result = await service.getDashboardPermissions('test-uid'); expect(result).toEqual(mockPermissions); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/dashboards/uid/test-uid/permissions'); }); }); describe('updateDashboardPermissions', () => { it('should update dashboard permissions', async () => { const permissions = [ { userId: 1, permission: 1 }, { teamId: 1, permission: 2 }, ]; vi.mocked(mockHttpClient.post).mockResolvedValue(undefined); await service.updateDashboardPermissions('test-uid', permissions); expect(mockHttpClient.post).toHaveBeenCalledWith( '/api/dashboards/uid/test-uid/permissions', { items: permissions } ); }); }); describe('getDashboardVersions', () => { it('should get dashboard versions', async () => { const mockVersions = [ { id: 1, version: 1, created: '2023-01-01T00:00:00Z' }, { id: 2, version: 2, created: '2023-01-02T00:00:00Z' }, ]; vi.mocked(mockHttpClient.get).mockResolvedValue(mockVersions); const result = await service.getDashboardVersions('test-uid'); expect(result).toEqual(mockVersions); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/dashboards/uid/test-uid/versions'); }); }); describe('getDashboardVersion', () => { it('should get specific dashboard version', async () => { const mockVersion = { id: 1, version: 2, created: '2023-01-01T00:00:00Z', dashboard: { uid: 'test-uid', title: 'Test' }, }; vi.mocked(mockHttpClient.get).mockResolvedValue(mockVersion); const result = await service.getDashboardVersion('test-uid', 2); expect(result).toEqual(mockVersion); expect(mockHttpClient.get).toHaveBeenCalledWith('/api/dashboards/uid/test-uid/versions/2'); }); }); describe('restoreDashboardVersion', () => { it('should restore dashboard to specific version', async () => { const mockResponse = { id: 1, uid: 'test-uid', version: 2 }; vi.mocked(mockHttpClient.post).mockResolvedValue(mockResponse); const result = await service.restoreDashboardVersion('test-uid', 2); expect(result).toEqual(mockResponse); expect(mockHttpClient.post).toHaveBeenCalledWith( '/api/dashboards/uid/test-uid/restore', { version: 2 } ); }); }); describe('error handling', () => { it('should propagate HTTP client errors', async () => { const error = new Error('Network error'); vi.mocked(mockHttpClient.get).mockRejectedValue(error); await expect(service.searchDashboards()).rejects.toThrow('Network error'); }); it('should handle malformed dashboard data', async () => { const malformedDashboard = { dashboard: { panels: null, // Invalid panels data }, }; vi.mocked(mockHttpClient.get).mockResolvedValue(malformedDashboard); await expect(service.getDashboardPanelQueries('test-uid')).rejects.toThrow(); }); }); describe('edge cases', () => { it('should handle empty dashboard list', async () => { vi.mocked(mockHttpClient.get).mockResolvedValue([]); const result = await service.searchDashboards(); expect(result).toEqual([]); }); it('should handle dashboard with empty panels array', async () => { const dashboardWithNoPanels: DashboardDetail = { dashboard: { id: 1, uid: 'test-uid', title: 'Empty Dashboard', tags: [], timezone: 'UTC', panels: [], time: { from: 'now-1h', to: 'now' }, timepicker: {}, templating: { list: [] }, annotations: { list: [] }, refresh: '5s', schemaVersion: 30, version: 1, links: [], }, meta: {} as any, }; vi.mocked(mockHttpClient.get).mockResolvedValue(dashboardWithNoPanels); const result = await service.getDashboardPanelQueries('test-uid'); expect(result.panels).toEqual([]); }); it('should handle large dashboard queries', async () => { const largeDashboard = { dashboard: { panels: Array.from({ length: 100 }, (_, i) => ({ id: i, title: `Panel ${i}`, type: 'graph', targets: [ { refId: 'A', expr: `query_${i}`, queryType: 'prometheus', }, ], })), }, }; vi.mocked(mockHttpClient.get).mockResolvedValue(largeDashboard); const result = await service.getDashboardPanelQueries('test-uid'); expect(result.panels).toHaveLength(100); expect(result.panels[99].title).toBe('Panel 99'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/quanticsoul4772/grafana-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server