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 { BROWSER_MODE_DISABLED_MESSAGE } from './constants'; import InfluxDatasource from './datasource'; import { getMockDSInstanceSettings, getMockInfluxDS, mockBackendService, replaceMock } from './mocks/datasource'; import { mockInfluxQueryRequest } from './mocks/request'; import { mockInfluxFetchResponse, mockMetricFindQueryResponse } from './mocks/response'; 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); expect(result.adhocFilters![0].value).toBe(templateVarValue); }); });