unit tests

This commit is contained in:
ismail simsek
2025-05-27 15:47:57 +02:00
parent 31efa8383b
commit 7a7f7bfcbc
2 changed files with 210 additions and 242 deletions
@@ -536,82 +536,6 @@ describe('Prometheus Language Provider', () => {
});
});
describe('Error handling and metadata', () => {
it('should handle disabled metadata lookups gracefully', async () => {
const datasource: PrometheusDatasource = {
...defaultDatasource,
metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })),
lookupsDisabled: false,
} as unknown as PrometheusDatasource;
const mockedMetadataRequest = jest.mocked(datasource.metadataRequest);
const instance = new PrometheusLanguageProvider(datasource);
expect(mockedMetadataRequest.mock.calls.length).toBe(0);
await instance.start();
expect(mockedMetadataRequest.mock.calls.length).toBeGreaterThan(0);
});
it('should handle metadata request failures gracefully', async () => {
jest.spyOn(console, 'error').mockImplementation();
const datasource: PrometheusDatasource = {
...defaultDatasource,
metadataRequest: jest.fn(() => Promise.reject('rejected')),
lookupsDisabled: false,
} as unknown as PrometheusDatasource;
const mockedMetadataRequest = jest.mocked(datasource.metadataRequest);
const instance = new PrometheusLanguageProvider(datasource);
expect(mockedMetadataRequest.mock.calls.length).toBe(0);
const result = await instance.start();
expect(result[0]).toBeUndefined();
expect(result[1]).toEqual([]);
expect(mockedMetadataRequest.mock.calls.length).toBe(3);
});
it('should include cache headers for requests when cacheLevel is set', () => {
const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium);
const languageProvider = new PrometheusLanguageProvider({
...defaultDatasource,
cacheLevel: PrometheusCacheLevel.Medium,
} as PrometheusDatasource);
const fetchLabelValues = languageProvider.fetchLabelValues;
const requestSpy = jest.spyOn(languageProvider, 'request');
fetchLabelValues(getMockTimeRange(), 'job');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy.mock.calls[0][2]).toEqual({
headers: { 'X-Grafana-Cache': `private, max-age=${timeSnapMinutes * 60}` },
});
});
it('should handle request errors gracefully', async () => {
jest.spyOn(console, 'error').mockImplementation();
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
const datasourceRequestMock = jest
.spyOn(defaultDatasource, 'metadataRequest')
.mockRejectedValue(new Error('Network error'));
const result = await languageProvider.request('/api/v1/labels', {});
expect(datasourceRequestMock).toHaveBeenCalled();
expect(result).toEqual(undefined);
});
it('should ignore cancelled request errors', async () => {
jest.spyOn(console, 'error').mockImplementation();
const languageProvider = new PrometheusLanguageProvider(defaultDatasource);
const error = { cancelled: true };
const datasourceRequestMock = jest.spyOn(defaultDatasource, 'metadataRequest').mockRejectedValue(error);
const result = await languageProvider.request('/api/v1/labels', {});
expect(datasourceRequestMock).toHaveBeenCalled();
expect(result).toEqual(undefined);
expect(console.error).not.toHaveBeenCalled();
});
});
describe('fetchSuggestions', () => {
it('should send POST request with correct parameters', async () => {
const timeRange = getMockTimeRange();
@@ -694,171 +618,6 @@ describe('Prometheus Language Provider', () => {
);
});
});
describe('New PrometheusLanguageProvider Interface', () => {
let provider: PrometheusLanguageProviderInterface;
beforeEach(() => {
provider = new PrometheusLanguageProvider(defaultDatasource);
});
describe('Retrieve Methods (Synchronous)', () => {
beforeEach(async () => {
// Setup the data through public methods
// This is the raw API response shape
const rawMetadata = {
metric1: [
{
type: 'counter',
help: 'help text',
},
],
};
// Mock the request to return raw API shape
jest.spyOn(provider, 'request').mockResolvedValue(rawMetadata);
await provider.queryMetricsMetadata();
// Setup metrics through start method
const metrics = ['metric1', 'metric2'];
jest.spyOn(provider, 'request').mockResolvedValue(metrics);
await provider.start();
});
it('should retrieve metrics metadata', () => {
const result = provider.retrieveMetricsMetadata();
expect(result).toEqual(
expect.objectContaining({
metric1: {
type: 'counter',
help: 'help text',
},
})
);
});
it('should retrieve histogram metrics', () => {
const result = provider.retrieveHistogramMetrics();
expect(result).toEqual(expect.any(Array));
});
it('should retrieve metrics', () => {
const result = provider.retrieveMetrics();
expect(result).toEqual(expect.any(Array));
});
it('should retrieve label keys', () => {
const result = provider.retrieveLabelKeys();
expect(result).toEqual(expect.any(Array));
});
});
describe('Query Methods (Asynchronous)', () => {
const timeRange = getMockTimeRange();
it('should query metrics metadata', async () => {
// Raw API response shape
const rawMetadata = {
metric1: [
{
type: 'counter',
help: 'help text',
},
],
};
jest.spyOn(provider, 'request').mockResolvedValue(rawMetadata);
const result = await provider.queryMetricsMetadata();
expect(result).toEqual(
expect.objectContaining({
metric1: {
type: 'counter',
help: 'help text',
},
})
);
});
describe('queryLabelKeys', () => {
it('should use labels API when supported', async () => {
const labelKeys = ['label1', 'label2'];
const datasourceWithLabelsAPI = {
...defaultDatasource,
hasLabelsMatchAPISupport: () => true,
} as unknown as PrometheusDatasource;
provider = new PrometheusLanguageProvider(datasourceWithLabelsAPI);
jest.spyOn(provider, 'request').mockResolvedValue(labelKeys);
const result = await provider.queryLabelKeys(timeRange, '{job="grafana"}');
expect(result).toEqual(['label1', 'label2']);
});
it('should use series API when labels API not supported', async () => {
const series = [{ __name__: 'metric', label1: 'value1', label2: 'value2' }];
const datasourceWithoutLabelsAPI = {
...defaultDatasource,
hasLabelsMatchAPISupport: () => false,
} as unknown as PrometheusDatasource;
provider = new PrometheusLanguageProvider(datasourceWithoutLabelsAPI);
jest.spyOn(provider, 'request').mockResolvedValue(series);
const result = await provider.queryLabelKeys(timeRange, '{job="grafana"}');
expect(result).toEqual(['label1', 'label2']);
});
it('should handle empty series response', async () => {
jest.spyOn(provider, 'request').mockResolvedValue([]);
const result = await provider.queryLabelKeys(timeRange, '{job="grafana"}');
expect(result).toEqual([]);
});
});
describe('queryLabelValues', () => {
it('should use labels API when supported', async () => {
const labelValues = ['value1', 'value2'];
const datasourceWithLabelsAPI = {
...defaultDatasource,
hasLabelsMatchAPISupport: () => true,
} as unknown as PrometheusDatasource;
provider = new PrometheusLanguageProvider(datasourceWithLabelsAPI);
jest.spyOn(provider, 'request').mockResolvedValue(labelValues);
const result = await provider.queryLabelValues(timeRange, 'job', '{job="grafana"}');
expect(result).toEqual(['value1', 'value2']);
});
it('should use series API when labels API not supported', async () => {
const series = [
{ __name__: 'metric', job: 'value1' },
{ __name__: 'metric', job: 'value2' },
];
const datasourceWithoutLabelsAPI = {
...defaultDatasource,
hasLabelsMatchAPISupport: () => false,
} as unknown as PrometheusDatasource;
provider = new PrometheusLanguageProvider(datasourceWithoutLabelsAPI);
jest.spyOn(provider, 'request').mockResolvedValue(series);
const result = await provider.queryLabelValues(timeRange, 'job', '{job="grafana"}');
expect(result).toEqual(['value1', 'value2']);
});
it('should handle empty series response', async () => {
jest.spyOn(provider, 'request').mockResolvedValue([]);
const result = await provider.queryLabelValues(timeRange, 'job', '{job="grafana"}');
expect(result).toEqual([]);
});
});
});
});
});
describe('Query transformation', () => {
@@ -1,4 +1,213 @@
import { processSeries } from './resource_clients';
import { dateTime, TimeRange } from '@grafana/data';
import { LabelsApiClient, processSeries, SeriesApiClient } from './resource_clients';
import { PrometheusCacheLevel } from './types';
const mockTimeRange: TimeRange = {
from: dateTime(1681300292392),
to: dateTime(1681300293392),
raw: {
from: 'now-1s',
to: 'now',
},
};
const mockRequest = jest.fn();
const mockGetAdjustedInterval = jest.fn().mockReturnValue({
start: '1681300260',
end: '1681300320',
});
const mockGetTimeRangeParams = jest.fn().mockReturnValue({
start: '1681300260',
end: '1681300320',
});
const mockInterpolateString = jest.fn((str) => str);
const defaultCacheHeaders = { headers: { 'X-Grafana-Cache': 'private, max-age=60' } };
describe('LabelsApiClient', () => {
let client: LabelsApiClient;
beforeEach(() => {
jest.clearAllMocks();
client = new LabelsApiClient(
mockRequest,
PrometheusCacheLevel.Low,
mockGetAdjustedInterval,
mockGetTimeRangeParams,
mockInterpolateString
);
});
describe('start', () => {
it('should initialize metrics and label keys', async () => {
mockRequest.mockResolvedValueOnce(['metric1', 'metric2']).mockResolvedValueOnce(['label1', 'label2']);
await client.start(mockTimeRange);
expect(client.metrics).toEqual(['metric1', 'metric2']);
expect(client.labelKeys).toEqual(['label1', 'label2']);
});
});
describe('queryMetrics', () => {
it('should fetch metrics and process histogram metrics', async () => {
mockRequest.mockResolvedValueOnce(['metric1_bucket', 'metric2_sum', 'metric3_count']);
const result = await client.queryMetrics(mockTimeRange);
expect(result.metrics).toEqual(['metric1_bucket', 'metric2_sum', 'metric3_count']);
expect(result.histogramMetrics).toEqual(['metric1_bucket']);
});
});
describe('queryLabelKeys', () => {
it('should fetch and sort label keys', async () => {
mockRequest.mockResolvedValueOnce(['label2', 'label1', 'label3']);
const result = await client.queryLabelKeys(mockTimeRange);
expect(result).toEqual(['label1', 'label2', 'label3']);
expect(mockRequest).toHaveBeenCalledWith(
'/api/v1/labels',
{
limit: '40000',
start: expect.any(String),
end: expect.any(String),
},
defaultCacheHeaders
);
});
it('should include match parameter when provided', async () => {
mockRequest.mockResolvedValueOnce(['label1', 'label2']);
await client.queryLabelKeys(mockTimeRange, '{job="grafana"}');
expect(mockRequest).toHaveBeenCalledWith(
'/api/v1/labels',
{
'match[]': '{job="grafana"}',
limit: '40000',
start: expect.any(String),
end: expect.any(String),
},
defaultCacheHeaders
);
});
});
describe('queryLabelValues', () => {
it('should fetch label values with proper encoding', async () => {
mockRequest.mockResolvedValueOnce(['value1', 'value2']);
mockInterpolateString.mockImplementationOnce((str) => str);
const result = await client.queryLabelValues(mockTimeRange, 'job');
expect(result).toEqual(['value1', 'value2']);
expect(mockRequest).toHaveBeenCalledWith(
'/api/v1/label/job/values',
{
start: expect.any(String),
end: expect.any(String),
limit: '40000',
},
defaultCacheHeaders
);
});
it('should handle UTF-8 label names', async () => {
mockRequest.mockResolvedValueOnce(['value1', 'value2']);
mockInterpolateString.mockImplementationOnce((str) => 'http.status:sum');
await client.queryLabelValues(mockTimeRange, '"http.status:sum"');
expect(mockRequest).toHaveBeenCalledWith(
'/api/v1/label/U__http_2e_status:sum/values',
{
start: expect.any(String),
end: expect.any(String),
limit: '40000',
},
defaultCacheHeaders
);
});
});
});
describe('SeriesApiClient', () => {
let client: SeriesApiClient;
beforeEach(() => {
jest.clearAllMocks();
client = new SeriesApiClient(
mockRequest,
PrometheusCacheLevel.Low,
mockGetAdjustedInterval,
mockGetTimeRangeParams,
mockInterpolateString
);
});
describe('start', () => {
it('should initialize metrics and histogram metrics', async () => {
mockRequest.mockResolvedValueOnce([{ __name__: 'metric1_bucket' }, { __name__: 'metric2_sum' }]);
await client.start(mockTimeRange);
expect(client.metrics).toEqual(['metric1_bucket', 'metric2_sum']);
expect(client.histogramMetrics).toEqual(['metric1_bucket']);
});
});
describe('queryMetrics', () => {
it('should fetch and process series data', async () => {
mockRequest.mockResolvedValueOnce([
{ __name__: 'metric1', label1: 'value1' },
{ __name__: 'metric2', label2: 'value2' },
]);
const result = await client.queryMetrics(mockTimeRange);
expect(result.metrics).toEqual(['metric1', 'metric2']);
expect(client.labelKeys).toEqual(['label1', 'label2']);
});
});
describe('queryLabelKeys', () => {
it('should throw error if match parameter is not provided', async () => {
await expect(client.queryLabelKeys(mockTimeRange)).rejects.toThrow(
'Series endpoint always expects at least one matcher'
);
});
it('should fetch and process label keys from series', async () => {
mockRequest.mockResolvedValueOnce([{ __name__: 'metric1', label1: 'value1', label2: 'value2' }]);
const result = await client.queryLabelKeys(mockTimeRange, '{job="grafana"}');
expect(result).toEqual(['label1', 'label2']);
});
});
describe('queryLabelValues', () => {
it('should throw error if match parameter is not provided', async () => {
await expect(client.queryLabelValues(mockTimeRange, 'job')).rejects.toThrow(
'Series endpoint always expects at least one matcher'
);
});
it('should fetch and process label values from series', async () => {
mockRequest.mockResolvedValueOnce([
{ __name__: 'metric1', job: 'grafana' },
{ __name__: 'metric2', job: 'prometheus' },
]);
const result = await client.queryLabelValues(mockTimeRange, 'job', '{__name__="metric1"}');
expect(result).toEqual(['grafana', 'prometheus']);
});
});
});
describe('processSeries', () => {
it('should extract metrics and label keys from series data', () => {