diff --git a/.betterer.results b/.betterer.results index c032091d058..ee1f2b81249 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6367,6 +6367,12 @@ exports[`better eslint`] = { "public/app/plugins/datasource/influxdb/migrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/plugins/datasource/influxdb/mocks.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"] + ], "public/app/plugins/datasource/influxdb/query_part.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -6390,24 +6396,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/plugins/datasource/influxdb/specs/datasource.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"] - ], - "public/app/plugins/datasource/influxdb/specs/mocks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"] - ], "public/app/plugins/datasource/jaeger/CheatSheet.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"] diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx index 28a66596db9..7a89362d515 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tags.test.tsx @@ -12,11 +12,11 @@ jest.mock('../../../../../influxql_metadata_query', () => { return { __esModule: true, getAllPolicies: jest.fn().mockReturnValueOnce(Promise.resolve(['default', 'autogen'])), - getFieldKeysForMeasurement: jest + getFieldKeys: jest .fn() .mockReturnValueOnce(Promise.resolve(['free', 'total'])) .mockReturnValueOnce(Promise.resolve([])), - getTagKeysForMeasurementAndTags: jest + getTagKeys: jest .fn() // first time we are called when the widget mounts, // we respond by saying `cpu, host, device` are the real tags @@ -31,7 +31,7 @@ jest.mock('../../../../../influxql_metadata_query', () => { // it does not matter what we return, as long as it is // promise-of-a-list-of-strings .mockReturnValueOnce(Promise.resolve([])), - getAllMeasurementsForTags: jest + getAllMeasurements: jest .fn() // it does not matter what we return, as long as it is // promise-of-a-list-of-strings @@ -48,8 +48,8 @@ jest.mock('@grafana/runtime', () => { }); beforeEach(() => { - (mockedMeta.getTagKeysForMeasurementAndTags as jest.Mock).mockClear(); - (mockedMeta.getFieldKeysForMeasurement as jest.Mock).mockClear(); + (mockedMeta.getTagKeys as jest.Mock).mockClear(); + (mockedMeta.getFieldKeys as jest.Mock).mockClear(); }); const ONLY_TAGS = [ @@ -128,11 +128,11 @@ describe('InfluxDB InfluxQL Visual Editor field-filtering', () => { await waitFor(() => {}); - // when the editor-widget mounts, it calls getFieldKeysForMeasurement - expect(mockedMeta.getFieldKeysForMeasurement).toHaveBeenCalledTimes(1); + // when the editor-widget mounts, it calls getFieldKeys + expect(mockedMeta.getFieldKeys).toHaveBeenCalledTimes(1); - // when the editor-widget mounts, it calls getTagKeysForMeasurementAndTags - expect(mockedMeta.getTagKeysForMeasurementAndTags).toHaveBeenCalledTimes(1); + // when the editor-widget mounts, it calls getTagKeys + expect(mockedMeta.getTagKeys).toHaveBeenCalledTimes(1); // now we click on the WHERE/host2 button await userEvent.click(screen.getByRole('button', { name: 'host2' })); @@ -145,7 +145,7 @@ describe('InfluxDB InfluxQL Visual Editor field-filtering', () => { await userEvent.click(screen.getByRole('button', { name: 'cpudata' })); // verify `getTagValues` was called once, and in the tags-param we did not receive `field1` - expect(mockedMeta.getAllMeasurementsForTags).toHaveBeenCalledTimes(1); - expect((mockedMeta.getAllMeasurementsForTags as jest.Mock).mock.calls[0][1]).toStrictEqual(ONLY_TAGS); + expect(mockedMeta.getAllMeasurements).toHaveBeenCalledTimes(1); + expect((mockedMeta.getAllMeasurements as jest.Mock).mock.calls[0][1]).toStrictEqual(ONLY_TAGS); }); }); diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx index e19bb358e6a..3fc70dd0a0e 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.test.tsx @@ -2,7 +2,7 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; import InfluxDatasource from '../../../../../datasource'; -import { getMockDS, getMockDSInstanceSettings } from '../../../../../specs/mocks'; +import { getMockInfluxDS, getMockDSInstanceSettings } from '../../../../../mocks'; import { DEFAULT_POLICY, InfluxQuery } from '../../../../../types'; import { VisualInfluxQLEditor } from './VisualInfluxQLEditor'; @@ -39,7 +39,7 @@ jest.mock('./Seg', () => { async function assertEditor(query: InfluxQuery, textContent: string) { const onChange = jest.fn(); const onRunQuery = jest.fn(); - const datasource: InfluxDatasource = getMockDS(getMockDSInstanceSettings()); + const datasource: InfluxDatasource = getMockInfluxDS(getMockDSInstanceSettings()); datasource.metricFindQuery = () => Promise.resolve([]); const { container } = render( diff --git a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx index 3f4e4256c28..c300ce9c5dc 100644 --- a/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/editor/query/influxql/visual/VisualInfluxQLEditor.tsx @@ -6,10 +6,10 @@ import { InlineLabel, SegmentSection, useStyles2 } from '@grafana/ui'; import InfluxDatasource from '../../../../../datasource'; import { - getAllMeasurementsForTags, + getAllMeasurements, getAllPolicies, - getFieldKeysForMeasurement, - getTagKeysForMeasurementAndTags, + getFieldKeys, + getTagKeys, getTagValues, } from '../../../../../influxql_metadata_query'; import { @@ -53,13 +53,9 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => { const { measurement, policy } = query; const allTagKeys = useMemo(async () => { - const tagKeys = (await getTagKeysForMeasurementAndTags(datasource, [], measurement, policy)).map( - (tag) => `${tag}::tag` - ); + const tagKeys = (await getTagKeys(datasource, measurement, policy)).map((tag) => `${tag}::tag`); - const fieldKeys = (await getFieldKeysForMeasurement(datasource, measurement || '', policy)).map( - (field) => `${field}::field` - ); + const fieldKeys = (await getFieldKeys(datasource, measurement || '', policy)).map((field) => `${field}::field`); return new Set([...tagKeys, ...fieldKeys]); }, [measurement, policy, datasource]); @@ -69,9 +65,7 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => { [ 'field_0', () => { - return measurement !== undefined - ? getFieldKeysForMeasurement(datasource, measurement, policy) - : Promise.resolve([]); + return measurement !== undefined ? getFieldKeys(datasource, measurement, policy) : Promise.resolve([]); }, ], ]); @@ -80,7 +74,7 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => { // the following function is not complicated enough to memoize, but it's result // is used in both memoized and un-memoized parts, so we have no choice - const getTagKeys = useMemo( + const getMemoizedTagKeys = useMemo( () => async () => { return [...(await allTagKeys)]; }, @@ -88,10 +82,10 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => { ); const groupByList = useMemo(() => { - const dynamicGroupByPartOptions = new Map([['tag_0', getTagKeys]]); + const dynamicGroupByPartOptions = new Map([['tag_0', getMemoizedTagKeys]]); return makePartList(query.groupBy ?? [], dynamicGroupByPartOptions); - }, [getTagKeys, query.groupBy]); + }, [getMemoizedTagKeys, query.groupBy]); const onAppliedChange = (newQuery: InfluxQuery) => { props.onChange(newQuery); @@ -123,11 +117,7 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => { getMeasurementOptions={(filter) => withTemplateVariableOptions( allTagKeys.then((keys) => - getAllMeasurementsForTags( - datasource, - filterTags(query.tags ?? [], keys), - filter === '' ? undefined : filter - ) + getAllMeasurements(datasource, filterTags(query.tags ?? [], keys), filter === '' ? undefined : filter) ), wrapRegex, filter @@ -141,7 +131,7 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => { withTemplateVariableOptions( allTagKeys.then((keys) => getTagValues(datasource, filterTags(query.tags ?? [], keys), key)), @@ -171,7 +161,7 @@ export const VisualInfluxQLEditor = (props: Props): JSX.Element => { getNewGroupByPartOptions(query, getTagKeys)} + getNewPartOptions={() => getNewGroupByPartOptions(query, getMemoizedTagKeys)} onChange={(partIndex, newParams) => { const newQuery = changeGroupByPart(query, partIndex, newParams); onAppliedChange(newQuery); diff --git a/public/app/plugins/datasource/influxdb/datasource.mock.ts b/public/app/plugins/datasource/influxdb/datasource.mock.ts deleted file mode 100644 index 077ccad68d4..00000000000 --- a/public/app/plugins/datasource/influxdb/datasource.mock.ts +++ /dev/null @@ -1,55 +0,0 @@ -type FieldsDefinition = { - name: string; - // String type, usually something like 'string' or 'float'. - type: string; -}; -type Measurements = { [measurement: string]: FieldsDefinition[] }; -type FieldReturnValue = { text: string }; - -/** - * Datasource mock for influx. At the moment this only works for queries that should return measurements or their - * fields and no other functionality is implemented. - */ -export class InfluxDatasourceMock { - constructor(private measurements: Measurements) {} - - metricFindQuery(query: string) { - if (isMeasurementsQuery(query)) { - return this.getMeasurements(); - } else { - return this.getMeasurementFields(query); - } - } - - private getMeasurements(): FieldReturnValue[] { - return Object.keys(this.measurements).map((key) => ({ text: key })); - } - - private getMeasurementFields(query: string): FieldReturnValue[] { - const match = query.match(/SHOW FIELD KEYS FROM \"(.+)\"/); - if (!match) { - throw new Error(`Failed to match query="${query}"`); - } - const measurementName = match[1]; - if (!measurementName) { - throw new Error(`Failed to match measurement name from query="${query}"`); - } - - const fields = this.measurements[measurementName]; - if (!fields) { - throw new Error( - `Failed to find measurement with name="${measurementName}" in measurements="[${Object.keys( - this.measurements - ).join(', ')}]"` - ); - } - - return fields.map((field) => ({ - text: field.name, - })); - } -} - -function isMeasurementsQuery(query: string) { - return /SHOW MEASUREMENTS/.test(query); -} diff --git a/public/app/plugins/datasource/influxdb/datasource.test.ts b/public/app/plugins/datasource/influxdb/datasource.test.ts new file mode 100644 index 00000000000..eeb2ebfbe19 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/datasource.test.ts @@ -0,0 +1,318 @@ +import { lastValueFrom, of } from 'rxjs'; + +import { ScopedVars } from '@grafana/data'; +import { BackendSrvRequest } from '@grafana/runtime/'; +import config from 'app/core/config'; + +import { TemplateSrv } from '../../../features/templating/template_srv'; + +import { BROWSER_MODE_DISABLED_MESSAGE } from './constants'; +import InfluxDatasource from './datasource'; +import { + getMockDSInstanceSettings, + getMockInfluxDS, + mockBackendService, + mockInfluxFetchResponse, + mockInfluxQueryRequest, + mockInfluxQueryWithTemplateVars, + mockTemplateSrv, +} from './mocks'; +import { InfluxQuery, InfluxVersion } from './types'; + +// we want only frontend mode in this file +config.featureToggles.influxdbBackendMigration = false; +const fetchMock = mockBackendService(mockInfluxFetchResponse()); + +describe('InfluxDataSource Frontend Mode', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + 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'); + } + } + }); + + describe('outdated browser mode', () => { + it('should throw an error when querying data', 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 with HTTP GET', () => { + 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', + }, + }; + + let requestQuery: string; + let requestMethod: string | undefined; + let requestData: string | null; + const fetchMockImpl = (req: BackendSrvRequest) => { + requestMethod = req.method; + requestQuery = req.params?.q; + requestData = req.data; + return of({ + data: { + status: 'success', + results: [ + { + series: [ + { + name: 'measurement', + columns: ['name'], + values: [['cpu']], + }, + ], + }, + ], + }, + }); + }; + + beforeEach(async () => { + jest.clearAllMocks(); + fetchMock.mockImplementation(fetchMockImpl); + }); + + it('should read the http method from jsonData', async () => { + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' })); + await ds.metricFindQuery(query, queryOptions); + expect(requestMethod).toBe('GET'); + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); + await ds.metricFindQuery(query, queryOptions); + expect(requestMethod).toBe('POST'); + }); + + it('should replace $timefilter', async () => { + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' })); + await ds.metricFindQuery(query, queryOptions); + expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms'); + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); + await ds.metricFindQuery(query, queryOptions); + expect(requestQuery).toBeFalsy(); + expect(requestData).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(query, queryOptions); + expect(requestData).toBeNull(); + }); + + it('should have data in request body if http mode is POST', async () => { + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); + await ds.metricFindQuery(query, queryOptions); + expect(requestData).not.toBeNull(); + expect(requestData).toMatch('q=SELECT'); + }); + + it('parse response correctly', async () => { + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'GET' })); + let responseGet = await ds.metricFindQuery(query, queryOptions); + expect(responseGet).toEqual([{ text: 'cpu' }]); + ds = getMockInfluxDS(getMockDSInstanceSettings({ httpMode: 'POST' })); + let responsePost = await ds.metricFindQuery(query, queryOptions); + expect(responsePost).toEqual([{ text: 'cpu' }]); + }); + }); + + describe('adhoc variables', () => { + const adhocFilters = [ + { + key: 'adhoc_key', + operator: '=', + value: 'adhoc_val', + condition: '', + }, + ]; + const mockTemplateService = new TemplateSrv(); + mockTemplateService.getAdhocFilters = jest.fn((_: string) => adhocFilters); + let ds = getMockInfluxDS(getMockDSInstanceSettings(), mockTemplateService); + it('query should contain the ad-hoc variable', () => { + ds.query(mockInfluxQueryRequest()); + const expected = encodeURIComponent( + 'SELECT mean("value") FROM "cpu" WHERE time >= 0ms and time <= 10ms AND "adhoc_key" = \'adhoc_val\' GROUP BY time($__interval) fill(null)' + ); + expect(fetchMock.mock.calls[0][0].data).toBe(`q=${expected}`); + }); + }); + + 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(); + }); + }); + + describe('variable interpolation', () => { + const text = 'interpolationText'; + const text2 = 'interpolationText2'; + const textWithoutFormatRegex = 'interpolationText,interpolationText2'; + const textWithFormatRegex = 'interpolationText|interpolationText2'; + const variableMap: Record = { + $interpolationVar: text, + $interpolationVar2: text2, + }; + const adhocFilters = [ + { + key: 'adhoc', + operator: '=', + value: 'val', + condition: '', + }, + ]; + const templateSrv = mockTemplateSrv( + jest.fn((_: string) => adhocFilters), + jest.fn((target?: string, scopedVars?: ScopedVars, format?: string | Function): string => { + if (!format) { + return variableMap[target!] || ''; + } + if (format === 'regex') { + return textWithFormatRegex; + } + return textWithoutFormatRegex; + }) + ); + const ds = new InfluxDatasource(getMockDSInstanceSettings(), templateSrv); + + function influxChecks(query: InfluxQuery) { + expect(templateSrv.replace).toBeCalledTimes(10); + expect(query.alias).toBe(text); + expect(query.measurement).toBe(textWithFormatRegex); + expect(query.policy).toBe(textWithFormatRegex); + expect(query.limit).toBe(textWithFormatRegex); + expect(query.slimit).toBe(textWithFormatRegex); + expect(query.tz).toBe(text); + expect(query.tags![0].value).toBe(textWithFormatRegex); + expect(query.groupBy![0].params![0]).toBe(textWithFormatRegex); + expect(query.select![0][0].params![0]).toBe(textWithFormatRegex); + expect(query.adhocFilters?.[0].key).toBe(adhocFilters[0].key); + } + + describe('when interpolating query variables for dashboard->explore', () => { + it('should interpolate all variables with Flux mode', () => { + ds.version = InfluxVersion.Flux; + const fluxQuery = { + refId: 'x', + query: '$interpolationVar,$interpolationVar2', + }; + const queries = ds.interpolateVariablesInQueries([fluxQuery], { + interpolationVar: { text: text, value: text }, + interpolationVar2: { text: text2, value: text2 }, + }); + expect(templateSrv.replace).toBeCalledTimes(1); + expect(queries[0].query).toBe(textWithFormatRegex); + }); + + it('should interpolate all variables with InfluxQL mode', () => { + ds.version = InfluxVersion.InfluxQL; + const queries = ds.interpolateVariablesInQueries([mockInfluxQueryWithTemplateVars(adhocFilters)], { + interpolationVar: { text: text, value: text }, + interpolationVar2: { text: text2, value: text2 }, + }); + influxChecks(queries[0]); + }); + }); + + describe('when interpolating template variables', () => { + it('should apply all template variables with Flux mode', () => { + ds.version = InfluxVersion.Flux; + const fluxQuery = { + refId: 'x', + query: '$interpolationVar', + }; + const query = ds.applyTemplateVariables(fluxQuery, { + interpolationVar: { + text: text, + value: text, + }, + }); + expect(templateSrv.replace).toBeCalledTimes(1); + expect(query.query).toBe(text); + }); + + it('should apply all template variables with InfluxQL mode', () => { + ds.version = ds.version = InfluxVersion.InfluxQL; + ds.access = 'proxy'; + config.featureToggles.influxdbBackendMigration = true; + const query = ds.applyTemplateVariables(mockInfluxQueryWithTemplateVars(adhocFilters), { + interpolationVar: { text: text, value: text }, + interpolationVar2: { text: 'interpolationText2', value: 'interpolationText2' }, + }); + influxChecks(query); + }); + + it('should apply all scopedVars to tags', () => { + ds.version = InfluxVersion.InfluxQL; + ds.access = 'proxy'; + config.featureToggles.influxdbBackendMigration = true; + const query = ds.applyTemplateVariables(mockInfluxQueryWithTemplateVars(adhocFilters), { + interpolationVar: { text: text, value: text }, + interpolationVar2: { text: 'interpolationText2', value: 'interpolationText2' }, + }); + expect(query.tags?.length).toBeGreaterThan(0); + const value = query.tags?.[0].value; + const scopedVars = 'interpolationText|interpolationText2'; + expect(value).toBe(scopedVars); + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 0bf71a00113..f19108fcbe4 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -220,7 +220,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { const { condition, ...asTag } = af; @@ -258,12 +258,12 @@ export default class InfluxDatasource extends DataSourceWithBackend { @@ -301,7 +301,7 @@ export default class InfluxDatasource extends DataSourceWithBackend { describe('adhoc filters', () => { let fetchReq: { queries: InfluxQuery[] }; const ctx = { - ds: getMockDS(getMockDSInstanceSettings(), templateSrv), + ds: getMockInfluxDS(getMockDSInstanceSettings(), templateSrv), }; beforeEach(async () => { fetchMock.mockImplementation((req) => { diff --git a/public/app/plugins/datasource/influxdb/specs/influx_query_model.test.ts b/public/app/plugins/datasource/influxdb/influx_query_model.test.ts similarity index 99% rename from public/app/plugins/datasource/influxdb/specs/influx_query_model.test.ts rename to public/app/plugins/datasource/influxdb/influx_query_model.test.ts index 0d32550114c..cd0bf8f878b 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_query_model.test.ts +++ b/public/app/plugins/datasource/influxdb/influx_query_model.test.ts @@ -1,6 +1,6 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; -import InfluxQueryModel from '../influx_query_model'; +import InfluxQueryModel from './influx_query_model'; describe('InfluxQuery', () => { const templateSrv = { replace: (val) => val } as TemplateSrv; diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts b/public/app/plugins/datasource/influxdb/influx_series.test.ts similarity index 99% rename from public/app/plugins/datasource/influxdb/specs/influx_series.test.ts rename to public/app/plugins/datasource/influxdb/influx_series.test.ts index 3c4d84f6cb9..eb603946520 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_series.test.ts +++ b/public/app/plugins/datasource/influxdb/influx_series.test.ts @@ -1,6 +1,6 @@ import { produce } from 'immer'; -import InfluxSeries from '../influx_series'; +import InfluxSeries from './influx_series'; describe('when generating timeseries from influxdb response', () => { describe('given multiple fields for series', () => { diff --git a/public/app/plugins/datasource/influxdb/influxql_metadata_query.test.ts b/public/app/plugins/datasource/influxdb/influxql_metadata_query.test.ts new file mode 100644 index 00000000000..e27429beb93 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/influxql_metadata_query.test.ts @@ -0,0 +1,273 @@ +import config from 'app/core/config'; + +import { getAllMeasurements, getAllPolicies, getFieldKeys, getTagKeys, getTagValues } from './influxql_metadata_query'; +import { getMockInfluxDS } from './mocks'; +import { InfluxQuery } from './types'; + +describe('influx_metadata_query', () => { + let query: string | undefined; + let target: InfluxQuery; + const mockMetricFindQuery = jest.fn(); + const mockRunMetadataQuery = jest.fn(); + mockMetricFindQuery.mockImplementation((q: string) => { + query = q; + return Promise.resolve([]); + }); + mockRunMetadataQuery.mockImplementation((t: InfluxQuery) => { + target = t; + query = t.query; + return Promise.resolve([]); + }); + + const ds = getMockInfluxDS(); + ds.metricFindQuery = mockMetricFindQuery; + ds.runMetadataQuery = mockRunMetadataQuery; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + // This should be removed when backend mode is default + describe('backend mode disabled', () => { + beforeEach(() => { + config.featureToggles.influxdbBackendMigration = false; + }); + + function frontendModeChecks() { + expect(mockRunMetadataQuery).not.toHaveBeenCalled(); + expect(mockMetricFindQuery).toHaveBeenCalled(); + } + + describe('getAllPolicies', () => { + it('should call metricFindQuery with SHOW RETENTION POLICIES', () => { + getAllPolicies(ds); + frontendModeChecks(); + expect(query).toMatch('SHOW RETENTION POLICIES'); + }); + }); + + describe('getAllMeasurements', () => { + it('no tags specified', () => { + getAllMeasurements(ds, []); + frontendModeChecks(); + expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); + }); + + it('with tags', () => { + getAllMeasurements(ds, [{ key: 'key', value: 'val' }]); + frontendModeChecks(); + expect(query).toMatch('SHOW MEASUREMENTS WHERE "key"'); + }); + + it('with measurement filter', () => { + getAllMeasurements(ds, [{ key: 'key', value: 'val' }], 'measurementFilter'); + frontendModeChecks(); + expect(query).toMatch('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)measurementFilter/ WHERE "key"'); + }); + }); + + describe('getTagKeys', () => { + it('no tags specified', () => { + getTagKeys(ds); + frontendModeChecks(); + expect(query).toBe('SHOW TAG KEYS'); + }); + + it('with measurement', () => { + getTagKeys(ds, 'test_measurement'); + frontendModeChecks(); + expect(query).toBe('SHOW TAG KEYS FROM "test_measurement"'); + }); + + it('with retention policy', () => { + getTagKeys(ds, 'test_measurement', 'rp'); + frontendModeChecks(); + expect(query).toBe('SHOW TAG KEYS FROM "rp"."test_measurement"'); + }); + }); + + describe('getTagValues', () => { + it('with key', () => { + getTagValues(ds, [], 'test_key'); + frontendModeChecks(); + expect(query).toBe('SHOW TAG VALUES WITH KEY = "test_key"'); + }); + + it('with key ends with ::tag', () => { + getTagValues(ds, [], 'test_key::tag'); + frontendModeChecks(); + expect(query).toBe('SHOW TAG VALUES WITH KEY = "test_key"'); + }); + + it('with key ends with ::field', async () => { + const result = await getTagValues(ds, [], 'test_key::field'); + expect(result.length).toBe(0); + }); + + it('with tags', () => { + getTagValues(ds, [{ key: 'tagKey', value: 'tag_val' }], 'test_key'); + frontendModeChecks(); + expect(query).toBe('SHOW TAG VALUES WITH KEY = "test_key" WHERE "tagKey" = \'tag_val\''); + }); + + it('with measurement', () => { + getTagValues(ds, [{ key: 'tagKey', value: 'tag_val' }], 'test_key', 'test_measurement'); + frontendModeChecks(); + expect(query).toBe( + 'SHOW TAG VALUES FROM "test_measurement" WITH KEY = "test_key" WHERE "tagKey" = \'tag_val\'' + ); + }); + + it('with retention policy', () => { + getTagValues(ds, [{ key: 'tagKey', value: 'tag_val' }], 'test_key', 'test_measurement', 'rp'); + frontendModeChecks(); + expect(query).toBe( + 'SHOW TAG VALUES FROM "rp"."test_measurement" WITH KEY = "test_key" WHERE "tagKey" = \'tag_val\'' + ); + }); + }); + + describe('getFieldKeys', () => { + it('with no retention policy', () => { + getFieldKeys(ds, 'test_measurement'); + frontendModeChecks(); + expect(query).toBe('SHOW FIELD KEYS FROM "test_measurement"'); + }); + + it('with empty measurement', () => { + getFieldKeys(ds, ''); + frontendModeChecks(); + expect(query).toBe('SHOW FIELD KEYS'); + }); + + it('with retention policy', () => { + getFieldKeys(ds, 'test_measurement', 'rp'); + frontendModeChecks(); + expect(query).toBe('SHOW FIELD KEYS FROM "rp"."test_measurement"'); + }); + }); + }); + + describe('backend mode enabled', () => { + beforeEach(() => { + config.featureToggles.influxdbBackendMigration = true; + }); + + function backendModeChecks() { + expect(mockMetricFindQuery).not.toHaveBeenCalled(); + expect(mockRunMetadataQuery).toHaveBeenCalled(); + expect(target).toBeDefined(); + expect(target.refId).toBe('metadataQuery'); + expect(target.rawQuery).toBe(true); + } + + describe('getAllPolicies', () => { + it('should call metricFindQuery with SHOW RETENTION POLICIES', () => { + getAllPolicies(ds); + backendModeChecks(); + expect(query).toMatch('SHOW RETENTION POLICIES'); + }); + }); + + describe('getAllMeasurements', () => { + it('no tags specified', () => { + getAllMeasurements(ds, []); + backendModeChecks(); + expect(query).toBe('SHOW MEASUREMENTS LIMIT 100'); + }); + + it('with tags', () => { + getAllMeasurements(ds, [{ key: 'key', value: 'val' }]); + backendModeChecks(); + expect(query).toMatch('SHOW MEASUREMENTS WHERE "key"'); + }); + + it('with measurement filter', () => { + getAllMeasurements(ds, [{ key: 'key', value: 'val' }], 'measurementFilter'); + backendModeChecks(); + expect(query).toMatch('SHOW MEASUREMENTS WITH MEASUREMENT =~ /(?i)measurementFilter/ WHERE "key"'); + }); + }); + + describe('getTagKeys', () => { + it('no tags specified', () => { + getTagKeys(ds); + backendModeChecks(); + expect(query).toBe('SHOW TAG KEYS'); + }); + + it('with measurement', () => { + getTagKeys(ds, 'test_measurement'); + backendModeChecks(); + expect(query).toBe('SHOW TAG KEYS FROM "test_measurement"'); + }); + + it('with retention policy', () => { + getTagKeys(ds, 'test_measurement', 'rp'); + backendModeChecks(); + expect(query).toBe('SHOW TAG KEYS FROM "rp"."test_measurement"'); + }); + }); + + describe('getTagValues', () => { + it('with key', () => { + getTagValues(ds, [], 'test_key'); + backendModeChecks(); + expect(query).toBe('SHOW TAG VALUES WITH KEY = "test_key"'); + }); + + it('with key ends with ::tag', () => { + getTagValues(ds, [], 'test_key::tag'); + backendModeChecks(); + expect(query).toBe('SHOW TAG VALUES WITH KEY = "test_key"'); + }); + + it('with key ends with ::field', async () => { + const result = await getTagValues(ds, [], 'test_key::field'); + expect(result.length).toBe(0); + }); + + it('with tags', () => { + getTagValues(ds, [{ key: 'tagKey', value: 'tag_val' }], 'test_key'); + backendModeChecks(); + expect(query).toBe('SHOW TAG VALUES WITH KEY = "test_key" WHERE "tagKey" = \'tag_val\''); + }); + + it('with measurement', () => { + getTagValues(ds, [{ key: 'tagKey', value: 'tag_val' }], 'test_key', 'test_measurement'); + backendModeChecks(); + expect(query).toBe( + 'SHOW TAG VALUES FROM "test_measurement" WITH KEY = "test_key" WHERE "tagKey" = \'tag_val\'' + ); + }); + + it('with retention policy', () => { + getTagValues(ds, [{ key: 'tagKey', value: 'tag_val' }], 'test_key', 'test_measurement', 'rp'); + backendModeChecks(); + expect(query).toBe( + 'SHOW TAG VALUES FROM "rp"."test_measurement" WITH KEY = "test_key" WHERE "tagKey" = \'tag_val\'' + ); + }); + }); + + describe('getFieldKeys', () => { + it('with no retention policy', () => { + getFieldKeys(ds, 'test_measurement'); + backendModeChecks(); + expect(query).toBe('SHOW FIELD KEYS FROM "test_measurement"'); + }); + + it('with empty measurement', () => { + getFieldKeys(ds, ''); + backendModeChecks(); + expect(query).toBe('SHOW FIELD KEYS'); + }); + + it('with retention policy', () => { + getFieldKeys(ds, 'test_measurement', 'rp'); + backendModeChecks(); + expect(query).toBe('SHOW FIELD KEYS FROM "rp"."test_measurement"'); + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/influxdb/influxql_metadata_query.ts b/public/app/plugins/datasource/influxdb/influxql_metadata_query.ts index 86c9a2cb232..b9395d7a2e4 100644 --- a/public/app/plugins/datasource/influxdb/influxql_metadata_query.ts +++ b/public/app/plugins/datasource/influxdb/influxql_metadata_query.ts @@ -49,7 +49,7 @@ export async function getAllPolicies(datasource: InfluxDatasource): Promise item.text); } -export async function getAllMeasurementsForTags( +export async function getAllMeasurements( datasource: InfluxDatasource, tags: InfluxQueryTag[], withMeasurementFilter?: string @@ -58,9 +58,8 @@ export async function getAllMeasurementsForTags( return data.map((item) => item.text); } -export async function getTagKeysForMeasurementAndTags( +export async function getTagKeys( datasource: InfluxDatasource, - tags: InfluxQueryTag[], measurement?: string, retentionPolicy?: string ): Promise { @@ -71,16 +70,17 @@ export async function getTagKeysForMeasurementAndTags( export async function getTagValues( datasource: InfluxDatasource, tags: InfluxQueryTag[], - tagKey: string, + withKey: string, measurement?: string, retentionPolicy?: string ): Promise { - if (tagKey.endsWith('::field')) { + if (withKey.endsWith('::field')) { return []; } const data = await runExploreQuery({ type: 'TAG_VALUES', - withKey: tagKey, + tags, + withKey, datasource, measurement, retentionPolicy, @@ -88,7 +88,7 @@ export async function getTagValues( return data.map((item) => item.text); } -export async function getFieldKeysForMeasurement( +export async function getFieldKeys( datasource: InfluxDatasource, measurement: string, retentionPolicy?: string diff --git a/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts b/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts index d02e2a4a38d..d5c70c4c572 100644 --- a/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts +++ b/public/app/plugins/datasource/influxdb/influxql_query_builder.test.ts @@ -1,5 +1,5 @@ import { buildMetadataQuery } from './influxql_query_builder'; -import { templateSrvStub as templateService } from './specs/mocks'; +import { templateSrvStub as templateService } from './mocks'; import { DEFAULT_POLICY } from './types'; describe('influxql-query-builder', () => { diff --git a/public/app/plugins/datasource/influxdb/specs/mocks.ts b/public/app/plugins/datasource/influxdb/mocks.ts similarity index 69% rename from public/app/plugins/datasource/influxdb/specs/mocks.ts rename to public/app/plugins/datasource/influxdb/mocks.ts index 085772bdfba..af3c0184c8d 100644 --- a/public/app/plugins/datasource/influxdb/specs/mocks.ts +++ b/public/app/plugins/datasource/influxdb/mocks.ts @@ -17,9 +17,10 @@ import { VariableInterpolation, } from '@grafana/runtime/src'; -import { TemplateSrv } from '../../../../features/templating/template_srv'; -import InfluxDatasource from '../datasource'; -import { InfluxOptions, InfluxQuery, InfluxVersion } from '../types'; +import { TemplateSrv } from '../../../features/templating/template_srv'; + +import InfluxDatasource from './datasource'; +import { InfluxOptions, InfluxQuery, InfluxVersion } from './types'; const getAdhocFiltersMock = jest.fn().mockImplementation(() => []); const replaceMock = jest.fn().mockImplementation((a: string, ...rest: unknown[]) => a); @@ -54,14 +55,16 @@ export function mockBackendService(response: FetchResponse) { return fetchMock; } -export function getMockDS( - instanceSettings: DataSourceInstanceSettings, +export function getMockInfluxDS( + instanceSettings: DataSourceInstanceSettings = getMockDSInstanceSettings(), templateSrv: TemplateSrv = templateSrvStub ): InfluxDatasource { return new InfluxDatasource(instanceSettings, templateSrv); } -export function getMockDSInstanceSettings(): DataSourceInstanceSettings { +export function getMockDSInstanceSettings( + overrideJsonData?: Partial +): DataSourceInstanceSettings { return { id: 123, url: 'proxied', @@ -91,7 +94,12 @@ export function getMockDSInstanceSettings(): DataSourceInstanceSettings -): Partial> => { - const defaults: DataQueryRequest = { - app: 'createDataRequest', +export const mockInfluxQueryRequest = (targets?: InfluxQuery[]): DataQueryRequest => { + return { + app: 'explore', interval: '1m', intervalMs: 60000, range: { @@ -241,8 +246,78 @@ export const mockInfluxDataRequest = ( requestId: '', scopedVars: {}, startTime: 0, - targets: targets, + targets: targets ?? mockTargets(), timezone: '', }; - return Object.assign(defaults, overrides ?? {}); }; + +export const mockTargets = (): InfluxQuery[] => { + return [ + { + refId: 'A', + datasource: { + type: 'influxdb', + uid: 'vA4bkHenk', + }, + policy: 'default', + resultFormat: 'time_series', + orderByTime: 'ASC', + tags: [], + groupBy: [ + { + type: 'time', + params: ['$__interval'], + }, + { + type: 'fill', + params: ['null'], + }, + ], + select: [ + [ + { + type: 'field', + params: ['value'], + }, + { + type: 'mean', + params: [], + }, + ], + ], + measurement: 'cpu', + }, + ]; +}; + +export const mockInfluxQueryWithTemplateVars = (adhocFilters: AdHocVariableFilter[]): InfluxQuery => ({ + refId: 'x', + alias: '$interpolationVar', + measurement: '$interpolationVar', + policy: '$interpolationVar', + limit: '$interpolationVar', + slimit: '$interpolationVar', + tz: '$interpolationVar', + tags: [ + { + key: 'cpu', + operator: '=~', + value: '/^$interpolationVar,$interpolationVar2$/', + }, + ], + groupBy: [ + { + params: ['$interpolationVar'], + type: 'tag', + }, + ], + select: [ + [ + { + params: ['$interpolationVar'], + type: 'field', + }, + ], + ], + adhocFilters, +}); diff --git a/public/app/plugins/datasource/influxdb/specs/query_part.test.ts b/public/app/plugins/datasource/influxdb/query_part.test.ts similarity index 99% rename from public/app/plugins/datasource/influxdb/specs/query_part.test.ts rename to public/app/plugins/datasource/influxdb/query_part.test.ts index 264c695c8a7..c49d2d2871d 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_part.test.ts +++ b/public/app/plugins/datasource/influxdb/query_part.test.ts @@ -1,4 +1,4 @@ -import queryPart from '../query_part'; +import queryPart from './query_part'; describe('InfluxQueryPart', () => { describe('series with measurement only', () => { diff --git a/public/app/plugins/datasource/influxdb/response_parser.test.ts b/public/app/plugins/datasource/influxdb/response_parser.test.ts index c10712c2b33..4a606d605ea 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.test.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.test.ts @@ -7,8 +7,8 @@ import config from 'app/core/config'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import InfluxQueryModel from './influx_query_model'; +import { getMockDSInstanceSettings, getMockInfluxDS } from './mocks'; import ResponseParser, { getSelectedParams } from './response_parser'; -import { getMockDS, getMockDSInstanceSettings } from './specs/mocks'; import { InfluxQuery } from './types'; jest.mock('@grafana/runtime', () => ({ @@ -319,7 +319,7 @@ describe('influxdb response parser', () => { describe('When issuing annotationQuery', () => { const ctx = { - ds: getMockDS(getMockDSInstanceSettings()), + ds: getMockInfluxDS(getMockDSInstanceSettings()), }; const fetchMock = jest.spyOn(backendSrv, 'fetch'); diff --git a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts b/public/app/plugins/datasource/influxdb/specs/datasource.test.ts deleted file mode 100644 index 95309d972aa..00000000000 --- a/public/app/plugins/datasource/influxdb/specs/datasource.test.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { lastValueFrom, of } from 'rxjs'; -import { TemplateSrvStub } from 'test/specs/helpers'; - -import { ScopedVars } from '@grafana/data/src'; -import { FetchResponse } from '@grafana/runtime'; -import config from 'app/core/config'; -import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ - -import { BROWSER_MODE_DISABLED_MESSAGE } from '../constants'; -import InfluxDatasource from '../datasource'; -import { InfluxQuery, InfluxVersion } from '../types'; - -//@ts-ignore -const templateSrv = new TemplateSrvStub(); - -jest.mock('@grafana/runtime', () => ({ - ...(jest.requireActual('@grafana/runtime') as unknown as object), - getBackendSrv: () => backendSrv, -})); - -describe('InfluxDataSource', () => { - const ctx: any = { - instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'GET' } }, - }; - - const fetchMock = jest.spyOn(backendSrv, 'fetch'); - - beforeEach(() => { - jest.clearAllMocks(); - ctx.instanceSettings.url = '/api/datasources/proxy/1'; - ctx.instanceSettings.access = 'proxy'; - ctx.ds = new InfluxDatasource(ctx.instanceSettings, templateSrv); - }); - - describe('When issuing metricFindQuery', () => { - const query = 'SELECT max(value) FROM measurement WHERE $timeFilter'; - const queryOptions = { - range: { - from: '2018-01-01T00:00:00Z', - to: '2018-01-02T00:00:00Z', - }, - }; - let requestQuery: any; - let requestMethod: string | undefined; - let requestData: any; - let response: any; - - beforeEach(async () => { - fetchMock.mockImplementation((req) => { - requestMethod = req.method; - requestQuery = req.params?.q; - requestData = req.data; - return of({ - data: { - status: 'success', - results: [ - { - series: [ - { - name: 'measurement', - columns: ['name'], - values: [['cpu']], - }, - ], - }, - ], - }, - } as FetchResponse); - }); - - response = await ctx.ds.metricFindQuery(query, queryOptions); - }); - - it('should replace $timefilter', () => { - expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms'); - }); - - it('should use the HTTP GET method', () => { - expect(requestMethod).toBe('GET'); - }); - - it('should not have any data in request body', () => { - expect(requestData).toBeNull(); - }); - - it('parse response correctly', () => { - expect(response).toEqual([{ text: 'cpu' }]); - }); - }); - - describe('When getting error on 200 after issuing a query', () => { - const queryOptions = { - range: { - from: '2018-01-01T00:00:00Z', - to: '2018-01-02T00:00:00Z', - }, - rangeRaw: { - from: '2018-01-01T00:00:00Z', - to: '2018-01-02T00:00:00Z', - }, - targets: [{}], - timezone: 'UTC', - scopedVars: { - interval: { text: '1m', value: '1m' }, - __interval: { text: '1m', value: '1m' }, - __interval_ms: { text: 60000, value: 60000 }, - }, - }; - - it('throws an error', async () => { - fetchMock.mockImplementation(() => { - return of({ - data: { - results: [ - { - error: 'Query timeout', - }, - ], - }, - } as FetchResponse); - }); - - ctx.ds.retentionPolicies = ['']; - - try { - await lastValueFrom(ctx.ds.query(queryOptions)); - } catch (err) { - if (err instanceof Error) { - expect(err.message).toBe('InfluxDB Error: Query timeout'); - } - } - }); - }); - - describe('When getting a request after issuing a query using outdated Browser Mode', () => { - beforeEach(() => { - jest.clearAllMocks(); - ctx.instanceSettings.url = '/api/datasources/proxy/1'; - ctx.instanceSettings.access = 'direct'; - ctx.ds = new InfluxDatasource(ctx.instanceSettings, templateSrv); - }); - - it('throws an error', async () => { - try { - await lastValueFrom(ctx.ds.query({})); - } catch (err) { - if (err instanceof Error) { - expect(err.message).toBe(BROWSER_MODE_DISABLED_MESSAGE); - } - } - }); - }); - - describe('InfluxDataSource in POST query mode', () => { - const ctx: any = { - instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'POST' } }, - }; - - beforeEach(() => { - ctx.instanceSettings.url = '/api/datasources/proxy/1'; - ctx.ds = new InfluxDatasource(ctx.instanceSettings, templateSrv); - }); - - describe('When issuing metricFindQuery', () => { - const query = 'SELECT max(value) FROM measurement'; - const queryOptions = {}; - let requestMethod: string | undefined; - let requestQueryParameter: Record | undefined; - let queryEncoded: any; - let requestQuery: any; - - beforeEach(async () => { - fetchMock.mockImplementation((req) => { - requestMethod = req.method; - requestQueryParameter = req.params; - requestQuery = req.data; - return of({ - data: { - results: [ - { - series: [ - { - name: 'measurement', - columns: ['max'], - values: [[1]], - }, - ], - }, - ], - }, - } as FetchResponse); - }); - - queryEncoded = await ctx.ds.serializeParams({ q: query }); - await ctx.ds.metricFindQuery(query, queryOptions).then(() => {}); - }); - - it('should have the query form urlencoded', () => { - expect(requestQuery).toBe(queryEncoded); - }); - - it('should use the HTTP POST method', () => { - expect(requestMethod).toBe('POST'); - }); - - it('should not have q as a query parameter', () => { - expect(requestQueryParameter).not.toHaveProperty('q'); - }); - }); - }); - - // Some functions are required by the parent datasource class to provide functionality - // such as ad-hoc filters, which requires the definition of the getTagKeys, and getTagValues - describe('Datasource contract', () => { - const metricFindQueryMock = jest.fn(); - beforeEach(() => { - ctx.ds.metricFindQuery = metricFindQueryMock; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('has function called getTagKeys', () => { - expect(Object.getOwnPropertyNames(Object.getPrototypeOf(ctx.ds))).toContain('getTagKeys'); - }); - - it('has function called getTagValues', () => { - expect(Object.getOwnPropertyNames(Object.getPrototypeOf(ctx.ds))).toContain('getTagValues'); - }); - - it('should be able to call getTagKeys without specifying any parameter', () => { - ctx.ds.getTagKeys(); - expect(metricFindQueryMock).toHaveBeenCalled(); - }); - - it('should be able to call getTagValues without specifying anything but key', () => { - ctx.ds.getTagValues({ key: 'test' }); - expect(metricFindQueryMock).toHaveBeenCalled(); - }); - }); - - describe('Variables should be interpolated correctly', () => { - const instanceSettings: any = {}; - const text = 'interpolationText'; - const text2 = 'interpolationText2'; - const textWithoutFormatRegex = 'interpolationText,interpolationText2'; - const textWithFormatRegex = 'interpolationText|interpolationText2'; - const variableMap: Record = { - $interpolationVar: text, - $interpolationVar2: text2, - }; - const adhocFilters = [ - { - key: 'adhoc', - operator: '=', - value: 'val', - condition: '', - }, - ]; - const templateSrv: any = { - getAdhocFilters: jest.fn((name: string) => { - return adhocFilters; - }), - replace: jest.fn((target?: string, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!format) { - return variableMap[target!] || ''; - } - if (format === 'regex') { - return textWithFormatRegex; - } - return textWithoutFormatRegex; - }), - }; - const ds = new InfluxDatasource(instanceSettings, templateSrv); - - const influxQuery = { - refId: 'x', - alias: '$interpolationVar', - measurement: '$interpolationVar', - policy: '$interpolationVar', - limit: '$interpolationVar', - slimit: '$interpolationVar', - tz: '$interpolationVar', - tags: [ - { - key: 'cpu', - operator: '=~', - value: '/^$interpolationVar,$interpolationVar2$/', - }, - ], - groupBy: [ - { - params: ['$interpolationVar'], - type: 'tag', - }, - ], - select: [ - [ - { - params: ['$interpolationVar'], - type: 'field', - }, - ], - ], - adhocFilters, - }; - - function influxChecks(query: InfluxQuery) { - expect(templateSrv.replace).toBeCalledTimes(10); - expect(query.alias).toBe(text); - expect(query.measurement).toBe(textWithFormatRegex); - expect(query.policy).toBe(textWithFormatRegex); - expect(query.limit).toBe(textWithFormatRegex); - expect(query.slimit).toBe(textWithFormatRegex); - expect(query.tz).toBe(text); - expect(query.tags![0].value).toBe(textWithFormatRegex); - expect(query.groupBy![0].params![0]).toBe(textWithFormatRegex); - expect(query.select![0][0].params![0]).toBe(textWithFormatRegex); - expect(query.adhocFilters?.[0].key).toBe(adhocFilters[0].key); - } - - describe('when interpolating query variables for dashboard->explore', () => { - it('should interpolate all variables with Flux mode', () => { - ds.version = InfluxVersion.Flux; - const fluxQuery = { - refId: 'x', - query: '$interpolationVar,$interpolationVar2', - }; - const queries = ds.interpolateVariablesInQueries([fluxQuery], { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: text2, value: text2 }, - }); - expect(templateSrv.replace).toBeCalledTimes(1); - expect(queries[0].query).toBe(textWithFormatRegex); - }); - - it('should interpolate all variables with InfluxQL mode', () => { - ds.version = InfluxVersion.InfluxQL; - const queries = ds.interpolateVariablesInQueries([influxQuery], { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: text2, value: text2 }, - }); - influxChecks(queries[0]); - }); - }); - - describe('when interpolating template variables', () => { - it('should apply all template variables with Flux mode', () => { - ds.version = InfluxVersion.Flux; - const fluxQuery = { - refId: 'x', - query: '$interpolationVar', - }; - const query = ds.applyTemplateVariables(fluxQuery, { - interpolationVar: { - text: text, - value: text, - }, - }); - expect(templateSrv.replace).toBeCalledTimes(1); - expect(query.query).toBe(text); - }); - - it('should apply all template variables with InfluxQL mode', () => { - ds.version = ds.version = InfluxVersion.InfluxQL; - ds.access = 'proxy'; - config.featureToggles.influxdbBackendMigration = true; - const query = ds.applyTemplateVariables(influxQuery, { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: 'interpolationText2', value: 'interpolationText2' }, - }); - influxChecks(query); - }); - - it('should apply all scopedVars to tags', () => { - ds.version = InfluxVersion.InfluxQL; - ds.access = 'proxy'; - config.featureToggles.influxdbBackendMigration = true; - const query = ds.applyTemplateVariables(influxQuery, { - interpolationVar: { text: text, value: text }, - interpolationVar2: { text: 'interpolationText2', value: 'interpolationText2' }, - }); - if (!query.tags?.length) { - throw new Error('Tags are not defined'); - } - const value = query.tags[0].value; - const scopedVars = 'interpolationText|interpolationText2'; - expect(value).toBe(scopedVars); - }); - }); - }); -});