The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/influxdb/datasource.test.ts

420 lines
15 KiB

import { lastValueFrom, of } from 'rxjs';
import { AdHocVariableFilter } from '@grafana/data';
import { BackendSrvRequest, TemplateSrv } from '@grafana/runtime';
import config from 'app/core/config';
import { queryBuilder } from '../../../features/variables/shared/testing/builders';
import { getMockDSInstanceSettings, getMockInfluxDS, mockBackendService, replaceMock } from './__mocks__/datasource';
import { mockInfluxQueryRequest } from './__mocks__/request';
import { mockInfluxFetchResponse, mockMetricFindQueryResponse } from './__mocks__/response';
import { BROWSER_MODE_DISABLED_MESSAGE } from './constants';
import InfluxDatasource from './datasource';
import { InfluxQuery, InfluxVersion } from './types';
const fetchMock = mockBackendService(mockInfluxFetchResponse());
describe('datasource initialization', () => {
it('should read the http method from jsonData', () => {
let ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' }));
expect(ds.httpMode).toBe('GET');
ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' }));
expect(ds.httpMode).toBe('POST');
});
});
// Remove this suite when influxdbBackendMigration feature toggle removed
describe('InfluxDataSource Frontend Mode [influxdbBackendMigration=false]', () => {
beforeEach(() => {
// we want only frontend mode in this suite
config.featureToggles.influxdbBackendMigration = false;
jest.clearAllMocks();
});
describe('general checks', () => {
it('should throw an error if there is 200 response with error', async () => {
const ds = getMockInfluxDS();
fetchMock.mockImplementation(() => {
return of({
data: {
results: [
{
error: 'Query timeout',
},
],
},
});
});
try {
await lastValueFrom(ds.query(mockInfluxQueryRequest()));
} catch (err) {
if (err instanceof Error) {
expect(err.message).toBe('InfluxDB Error: Query timeout');
}
}
});
it('should throw an error when querying data when deprecated access mode', async () => {
expect.assertions(1);
const instanceSettings = getMockDSInstanceSettings();
instanceSettings.access = 'direct';
const ds = getMockInfluxDS(instanceSettings);
try {
await lastValueFrom(ds.query(mockInfluxQueryRequest()));
} catch (err) {
if (err instanceof Error) {
expect(err.message).toBe(BROWSER_MODE_DISABLED_MESSAGE);
}
}
});
});
describe('metricFindQuery', () => {
let ds: InfluxDatasource;
const query = 'SELECT max(value) FROM measurement WHERE $timeFilter';
const queryOptions = {
range: {
from: '2018-01-01T00:00:00Z',
to: '2018-01-02T00:00:00Z',
},
};
const fetchMockImpl = (req: BackendSrvRequest) => {
return of({
data: {
status: 'success',
results: [
{
series: [
{
name: 'measurement',
columns: ['name'],
values: [['cpu']],
},
],
},
],
},
});
};
beforeEach(async () => {
jest.clearAllMocks();
fetchMock.mockImplementation(fetchMockImpl);
});
it('should replace $timefilter', async () => {
ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' }));
await ds.metricFindQuery({ refId: 'test', query }, queryOptions);
expect(fetchMock.mock.lastCall[0].params?.q).toMatch('time >= 1514764800000ms and time <= 1514851200000ms');
ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' }));
await ds.metricFindQuery({ refId: 'test', query }, queryOptions);
expect(fetchMock.mock.lastCall[0].params?.q).toBeFalsy();
expect(fetchMock.mock.lastCall[0].data).toMatch(
'time%20%3E%3D%201514764800000ms%20and%20time%20%3C%3D%201514851200000ms'
);
});
it('should not have any data in request body if http mode is GET', async () => {
ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' }));
await ds.metricFindQuery({ refId: 'test', query }, queryOptions);
expect(fetchMock.mock.lastCall[0].data).toBeNull();
});
it('should have data in request body if http mode is POST', async () => {
ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' }));
await ds.metricFindQuery({ refId: 'test', query }, queryOptions);
expect(fetchMock.mock.lastCall[0].data).not.toBeNull();
expect(fetchMock.mock.lastCall[0].data).toMatch('q=SELECT');
});
it('parse response correctly', async () => {
ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' }));
let responseGet = await ds.metricFindQuery({ refId: 'test', query }, queryOptions);
expect(responseGet).toEqual([{ text: 'cpu' }]);
ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' }));
let responsePost = await ds.metricFindQuery({ refId: 'test', query }, queryOptions);
expect(responsePost).toEqual([{ text: 'cpu' }]);
});
});
// Update this after starting to use TemplateSrv from @grafana/runtime package
describe('adhoc variables', () => {
let ds = getMockInfluxDS(getMockDSInstanceSettings());
it('query should contain the ad-hoc variable', () => {
ds.query(mockInfluxQueryRequest());
expect(replaceMock.mock.calls[0][0]).toBe('adhoc_val');
});
it('should make the fetch call for adhoc filter keys', () => {
fetchMock.mockReturnValue(
of({
results: [
{
statement_id: 0,
series: [
{
name: 'cpu',
columns: ['tagKey'],
values: [['datacenter'], ['geohash'], ['source']],
},
],
},
],
})
);
ds.getTagKeys();
expect(fetchMock).toHaveBeenCalled();
const fetchReq = fetchMock.mock.calls[0][0];
expect(fetchReq).not.toBeNull();
expect(fetchReq.data).toMatch(encodeURIComponent(`SHOW TAG KEYS`));
});
it('should make the fetch call for adhoc filter values', () => {
fetchMock.mockReturnValue(
of({
results: [
{
statement_id: 0,
series: [
{
name: 'mykey',
columns: ['key', 'value'],
values: [['mykey', 'value']],
},
],
},
],
})
);
ds.getTagValues({ key: 'mykey', filters: [] });
expect(fetchMock).toHaveBeenCalled();
const fetchReq = fetchMock.mock.calls[0][0];
expect(fetchReq).not.toBeNull();
expect(fetchReq.data).toMatch(encodeURIComponent(`SHOW TAG VALUES WITH KEY = "mykey"`));
});
});
describe('datasource contract', () => {
let ds: InfluxDatasource;
const metricFindQueryMock = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
ds = getMockInfluxDS();
ds.metricFindQuery = metricFindQueryMock;
});
afterEach(() => {
jest.clearAllMocks();
});
it('should check the datasource has "getTagKeys" function defined', () => {
expect(Object.getOwnPropertyNames(Object.getPrototypeOf(ds))).toContain('getTagKeys');
});
it('should check the datasource has "getTagValues" function defined', () => {
expect(Object.getOwnPropertyNames(Object.getPrototypeOf(ds))).toContain('getTagValues');
});
it('should be able to call getTagKeys without specifying any parameter', () => {
ds.getTagKeys();
expect(metricFindQueryMock).toHaveBeenCalled();
});
it('should be able to call getTagValues without specifying anything but key', () => {
ds.getTagValues({ key: 'test', filters: [] });
expect(metricFindQueryMock).toHaveBeenCalled();
});
it('should use dbName instead of database', () => {
const instanceSettings = getMockDSInstanceSettings();
instanceSettings.database = 'should_not_be_used';
ds = getMockInfluxDS(instanceSettings);
expect(ds.database).toBe('site');
});
it('should fallback to use use database is dbName is not exist', () => {
const instanceSettings = getMockDSInstanceSettings();
instanceSettings.database = 'fallback';
instanceSettings.jsonData.dbName = undefined;
ds = getMockInfluxDS(instanceSettings);
expect(ds.database).toBe('fallback');
});
});
});
describe('InfluxDataSource Backend Mode [influxdbBackendMigration=true]', () => {
beforeEach(() => {
// we want only backend mode in this suite
config.featureToggles.influxdbBackendMigration = true;
jest.clearAllMocks();
});
describe('metric find query', () => {
let ds = getMockInfluxDS(getMockDSInstanceSettings());
it('handles multiple frames', async () => {
const fetchMockImpl = () => {
return of(mockMetricFindQueryResponse);
};
fetchMock.mockImplementation(fetchMockImpl);
const values = await ds.getTagValues({ key: 'test_id', filters: [] });
expect(fetchMock).toHaveBeenCalled();
expect(values.length).toBe(5);
expect(values[0].text).toBe('test-t2-1');
});
});
});
describe('interpolateQueryExpr', () => {
const templateSrvStub = {
replace: jest.fn().mockImplementation((...rest: unknown[]) => 'templateVarReplaced'),
} as unknown as TemplateSrv;
let ds = getMockInfluxDS(getMockDSInstanceSettings(), templateSrvStub);
it('should return the value as it is', () => {
const value = 'normalValue';
const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build();
const result = ds.interpolateQueryExpr(value, variableMock, 'my query $tempVar');
const expectation = 'normalValue';
expect(result).toBe(expectation);
});
it('should return the escaped value if the value wrapped in regex', () => {
const value = '/special/path';
const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build();
const result = ds.interpolateQueryExpr(
value,
variableMock,
'select atan(z/sqrt(3.14)), that where path =~ /$tempVar/'
);
const expectation = `\\/special\\/path`;
expect(result).toBe(expectation);
});
it('should return the escaped value if the value wrapped in regex 2', () => {
const value = '/special/path';
const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build();
const result = ds.interpolateQueryExpr(
value,
variableMock,
'select atan(z/sqrt(3.14)), that where path !~ /^$tempVar$/'
);
const expectation = `\\/special\\/path`;
expect(result).toBe(expectation);
});
it('should return the escaped value if the value wrapped in regex 3', () => {
const value = ['env', 'env2', 'env3'];
const variableMock = queryBuilder()
.withId('tempVar')
.withName('tempVar')
.withMulti(false)
.withIncludeAll(true)
.build();
const result = ds.interpolateQueryExpr(
value,
variableMock,
'select atan(z/sqrt(3.14)), thing from path =~ /^($tempVar)$/'
);
const expectation = `(env|env2|env3)`;
expect(result).toBe(expectation);
});
it('should **not** return the escaped value if the value **is not** wrapped in regex', () => {
const value = '/special/path';
const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build();
const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`);
const expectation = `/special/path`;
expect(result).toBe(expectation);
});
it('should **not** return the escaped value if the value **is not** wrapped in regex 2', () => {
const value = '12.2';
const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build();
const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`);
const expectation = `12.2`;
expect(result).toBe(expectation);
});
it('should escape the value **always** if the variable is a multi-value variable', () => {
const value = [`/special/path`, `/some/other/path`];
const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti().build();
const result = ds.interpolateQueryExpr(value, variableMock, `select that where path = '$tempVar'`);
const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`;
expect(result).toBe(expectation);
});
it('should escape and join with the pipe even the variable is not multi-value', () => {
const variableMock = queryBuilder()
.withId('tempVar')
.withName('tempVar')
.withCurrent('All', '$__all')
.withMulti(false)
.withAllValue('')
.withIncludeAll()
.withOptions(
{
text: 'All',
value: '$__all',
},
{
text: `/special/path`,
value: `/special/path`,
},
{
text: `/some/other/path`,
value: `/some/other/path`,
}
)
.build();
const value = [`/special/path`, `/some/other/path`];
const result = ds.interpolateQueryExpr(value, variableMock, `select that where path =~ /$tempVar/`);
const expectation = `(\\/special\\/path|\\/some\\/other\\/path)`;
expect(result).toBe(expectation);
});
it('should **not** return the escaped value if the value **is not** wrapped in regex and the query is more complex (e.g. text is contained between two / but not a regex', () => {
const value = 'testmatch';
const variableMock = queryBuilder().withId('tempVar').withName('tempVar').withMulti(false).build();
const result = ds.interpolateQueryExpr(
value,
variableMock,
`select value where ("tag"::tag =~ /value/) AND where other = $tempVar $timeFilter GROUP BY time($__interval) tz('Europe/London')`
);
const expectation = `testmatch`;
expect(result).toBe(expectation);
});
it('should return floating point number as it is', () => {
const variableMock = queryBuilder()
.withId('tempVar')
.withName('tempVar')
.withMulti(false)
.withOptions({
text: `1.0`,
value: `1.0`,
})
.build();
const value = `1.0`;
const result = ds.interpolateQueryExpr(value, variableMock, `select value / $tempVar from /^measurement$/`);
const expectation = `1.0`;
expect(result).toBe(expectation);
});
it('template var in adhoc', () => {
const templateVarName = '$templateVarName';
const templateVarValue = 'templateVarValue';
const templateSrvStub = {
replace: jest
.fn()
.mockImplementation((target?: string) => (target === templateVarName ? templateVarValue : target)),
} as unknown as TemplateSrv;
const ds = getMockInfluxDS(getMockDSInstanceSettings(), templateSrvStub);
ds.version = InfluxVersion.SQL;
const adhocFilter: AdHocVariableFilter[] = [{ key: 'bar', value: templateVarName, operator: '=' }];
const result = ds.applyTemplateVariables(mockInfluxQueryRequest() as unknown as InfluxQuery, {}, adhocFilter);
expect(result.tags![0].value).toBe(templateVarValue);
});
});