From 32d4c8c6bcc1a15353d3010f64a04cdeb4b1d141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Thu, 19 Nov 2020 14:10:38 +0100 Subject: [PATCH] CloudWatch: Support request cancellation properly (#28865) --- .../datasource/cloudwatch/datasource.test.ts | 91 ++-- .../datasource/cloudwatch/datasource.ts | 180 +++--- .../cloudwatch/language_provider.ts | 20 +- .../cloudwatch/specs/datasource.test.ts | 513 +++++++++--------- 4 files changed, 425 insertions(+), 379 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 926ce3f16e9..26c67fee8c3 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -1,88 +1,101 @@ -import { DataQueryResponse, dateTime, DefaultTimeRange } from '@grafana/data'; +import { of } from 'rxjs'; import { setBackendSrv } from '@grafana/runtime'; +import { dateTime, DefaultTimeRange, observableTester } from '@grafana/data'; + import { TemplateSrv } from '../../../features/templating/template_srv'; import { CloudWatchDatasource } from './datasource'; describe('datasource', () => { describe('query', () => { - it('should return error if log query and log groups is not specified', async () => { + it('should return error if log query and log groups is not specified', done => { const { datasource } = setup(); - const response: DataQueryResponse = (await datasource - .query({ + + observableTester().subscribeAndExpectOnNext({ + observable: datasource.query({ targets: [ { queryMode: 'Logs' as 'Logs', }, ], - } as any) - .toPromise()) as any; - expect(response.error?.message).toBe('Log group is required'); + } as any), + expect: response => { + expect(response.error?.message).toBe('Log group is required'); + }, + done, + }); }); - it('should return empty response if queries are hidden', async () => { + it('should return empty response if queries are hidden', done => { const { datasource } = setup(); - const response: DataQueryResponse = (await datasource - .query({ + + observableTester().subscribeAndExpectOnNext({ + observable: datasource.query({ targets: [ { queryMode: 'Logs' as 'Logs', hide: true, }, ], - } as any) - .toPromise()) as any; - expect(response.data).toEqual([]); + } as any), + expect: response => { + expect(response.data).toEqual([]); + }, + done, + }); }); }); describe('performTimeSeriesQuery', () => { - it('should return the same length of data as result', async () => { - const { datasource } = setup(); - const awsRequestMock = jest.spyOn(datasource, 'awsRequest'); - const buildCloudwatchConsoleUrlMock = jest.spyOn(datasource, 'buildCloudwatchConsoleUrl'); - buildCloudwatchConsoleUrlMock.mockImplementation(() => ''); - awsRequestMock.mockImplementation(async () => { - return { + it('should return the same length of data as result', done => { + const { datasource } = setup({ + data: { results: { a: { refId: 'a', series: [{ name: 'cpu', points: [1, 1] }], meta: { gmdMeta: '' } }, b: { refId: 'b', series: [{ name: 'memory', points: [2, 2] }], meta: { gmdMeta: '' } }, }, - }; + }, + }); + const buildCloudwatchConsoleUrlMock = jest.spyOn(datasource, 'buildCloudwatchConsoleUrl'); + buildCloudwatchConsoleUrlMock.mockImplementation(() => ''); + + observableTester().subscribeAndExpectOnNext({ + observable: datasource.performTimeSeriesQuery( + { + queries: [ + { datasourceId: 1, refId: 'a' }, + { datasourceId: 1, refId: 'b' }, + ], + } as any, + { from: dateTime(), to: dateTime() } as any + ), + expect: response => { + expect(response.data.length).toEqual(2); + }, + done, }); - const response: DataQueryResponse = await datasource.performTimeSeriesQuery( - { - queries: [ - { datasourceId: 1, refId: 'a' }, - { datasourceId: 1, refId: 'b' }, - ], - } as any, - { from: dateTime(), to: dateTime() } as any - ); - expect(response.data.length).toEqual(2); }); }); describe('describeLogGroup', () => { it('replaces region correctly in the query', async () => { - const { datasource, datasourceRequestMock } = setup(); + const { datasource, fetchMock } = setup(); await datasource.describeLogGroups({ region: 'default' }); - expect(datasourceRequestMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1'); + expect(fetchMock.mock.calls[0][0].data.queries[0].region).toBe('us-west-1'); await datasource.describeLogGroups({ region: 'eu-east' }); - expect(datasourceRequestMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east'); + expect(fetchMock.mock.calls[1][0].data.queries[0].region).toBe('eu-east'); }); }); }); -function setup() { +function setup({ data = [] }: { data?: any } = {}) { const datasource = new CloudWatchDatasource({ jsonData: { defaultRegion: 'us-west-1' } } as any, new TemplateSrv(), { timeRange() { return DefaultTimeRange; }, } as any); - const datasourceRequestMock = jest.fn(); - datasourceRequestMock.mockResolvedValue({ data: [] }); - setBackendSrv({ datasourceRequest: datasourceRequestMock } as any); + const fetchMock = jest.fn().mockReturnValue(of({ data })); + setBackendSrv({ fetch: fetchMock } as any); - return { datasource, datasourceRequestMock }; + return { datasource, fetchMock }; } diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 9fb183a91c3..2d3827338db 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -1,7 +1,7 @@ import React from 'react'; import angular from 'angular'; import _ from 'lodash'; -import { from, merge, Observable, of, zip } from 'rxjs'; +import { merge, Observable, of, throwError, zip } from 'rxjs'; import { catchError, concatMap, @@ -184,7 +184,7 @@ export class CloudWatchDatasource extends DataSourceApi { const channelName: string = response.results['A'].meta.channelName; const channel = getGrafanaLiveSrv().getChannel({ @@ -310,11 +310,17 @@ export class CloudWatchDatasource extends DataSourceApi + queryParams: Array<{ + queryId: string; + refId: string; + limit?: number; + region: string; + statsGroups?: string[]; + }> ): Observable { this.logQueries = {}; queryParams.forEach(param => { @@ -587,74 +593,76 @@ export class CloudWatchDatasource extends DataSourceApi { - try { - const res: TSDBResponse = await this.awsRequest(TSDB_QUERY_ENDPOINT, request); - const dataframes: DataFrame[] = toDataQueryResponse({ data: res }).data; - if (!dataframes || dataframes.length <= 0) { - return { data: [] }; - } - - const data = dataframes.map(frame => { - const queryResult = res.results[frame.refId!]; - const error = queryResult.error ? { message: queryResult.error } : null; - if (!queryResult) { - return { frame, error }; + performTimeSeriesQuery(request: MetricRequest, { from, to }: TimeRange): Observable { + return this.awsRequest(TSDB_QUERY_ENDPOINT, request).pipe( + map(res => { + const dataframes: DataFrame[] = toDataQueryResponse({ data: res }).data; + if (!dataframes || dataframes.length <= 0) { + return { data: [] }; } - const requestQuery = request.queries.find(q => q.refId === frame.refId!) as any; - - const link = this.buildCloudwatchConsoleUrl( - requestQuery!, - from.toISOString(), - to.toISOString(), - frame.refId!, - queryResult.meta.gmdMeta - ); - - if (link) { - for (const field of frame.fields) { - field.config.links = [ - { - url: link, - title: 'View in CloudWatch console', - targetBlank: true, - }, - ]; + const data = dataframes.map(frame => { + const queryResult = res.results[frame.refId!]; + const error = queryResult.error ? { message: queryResult.error } : null; + if (!queryResult) { + return { frame, error }; } + + const requestQuery = request.queries.find(q => q.refId === frame.refId!) as any; + + const link = this.buildCloudwatchConsoleUrl( + requestQuery!, + from.toISOString(), + to.toISOString(), + frame.refId!, + queryResult.meta.gmdMeta + ); + + if (link) { + for (const field of frame.fields) { + field.config.links = [ + { + url: link, + title: 'View in CloudWatch console', + targetBlank: true, + }, + ]; + } + } + return { frame, error }; + }); + + return { + data: data.map(o => o.frame), + error: data + .map(o => o.error) + .reduce((err, error) => { + return err || error; + }, null), + }; + }), + catchError(err => { + if (/^Throttling:.*/.test(err.data.message)) { + const failedRedIds = Object.keys(err.data.results); + const regionsAffected = Object.values(request.queries).reduce( + (res: string[], { refId, region }) => + (refId && !failedRedIds.includes(refId)) || res.includes(region) ? res : [...res, region], + [] + ) as string[]; + + regionsAffected.forEach(region => this.debouncedAlert(this.datasourceName, this.getActualRegion(region))); } - return { frame, error }; - }); - return { - data: data.map(o => o.frame), - error: data - .map(o => o.error) - .reduce((err, error) => { - return err || error; - }, null), - }; - } catch (err) { - if (/^Throttling:.*/.test(err.data.message)) { - const failedRedIds = Object.keys(err.data.results); - const regionsAffected = Object.values(request.queries).reduce( - (res: string[], { refId, region }) => - (refId && !failedRedIds.includes(refId)) || res.includes(region) ? res : [...res, region], - [] - ) as string[]; + if (err.data && err.data.message === 'Metric request error' && err.data.error) { + err.data.message = err.data.error; + } - regionsAffected.forEach(region => this.debouncedAlert(this.datasourceName, this.getActualRegion(region))); - } - - if (err.data && err.data.message === 'Metric request error' && err.data.error) { - err.data.message = err.data.error; - } - - throw err; - } + return throwError(err); + }) + ); } - transformSuggestDataFromTable(suggestData: TSDBResponse) { + transformSuggestDataFromTable(suggestData: TSDBResponse): Array<{ text: any; label: any; value: any }> { return suggestData.results['metricFindQuery'].tables[0].rows.map(([text, value]) => ({ text, value, @@ -662,7 +670,7 @@ export class CloudWatchDatasource extends DataSourceApi> { const range = this.timeSrv.timeRange(); return this.awsRequest(TSDB_QUERY_ENDPOINT, { from: range.from.valueOf().toString(), @@ -678,9 +686,13 @@ export class CloudWatchDatasource extends DataSourceApi { - return this.transformSuggestDataFromTable(r); - }); + }) + .pipe( + map(r => { + return this.transformSuggestDataFromTable(r); + }) + ) + .toPromise(); } makeLogActionRequest( @@ -724,7 +736,7 @@ export class CloudWatchDatasource extends DataSourceApi toDataQueryResponse(val).data || []; - return from(this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams)).pipe( + return this.awsRequest(TSDB_QUERY_ENDPOINT, requestParams).pipe( map(response => resultsToDataFrames({ data: response })), catchError(err => { if (err.data?.error) { @@ -920,15 +932,19 @@ export class CloudWatchDatasource extends DataSourceApi { - return r.results['annotationQuery'].tables[0].rows.map(v => ({ - annotation: annotation, - time: Date.parse(v[0]), - title: v[1], - tags: [v[2]], - text: v[3], - })); - }); + }) + .pipe( + map(r => { + return r.results['annotationQuery'].tables[0].rows.map(v => ({ + annotation: annotation, + time: Date.parse(v[0]), + title: v[1], + tags: [v[2]], + text: v[3], + })); + }) + ) + .toPromise(); } targetContainsTemplate(target: any) { @@ -955,16 +971,16 @@ export class CloudWatchDatasource extends DataSourceApi { const options = { method: 'POST', url, data, }; - const result = await getBackendSrv().datasourceRequest(options); - - return result.data; + return getBackendSrv() + .fetch(options) + .pipe(map(result => result.data)); } getDefaultRegion() { diff --git a/public/app/plugins/datasource/cloudwatch/language_provider.ts b/public/app/plugins/datasource/cloudwatch/language_provider.ts index 0865a729e99..9bd85467466 100644 --- a/public/app/plugins/datasource/cloudwatch/language_provider.ts +++ b/public/app/plugins/datasource/cloudwatch/language_provider.ts @@ -3,22 +3,22 @@ import _ from 'lodash'; // Services & Utils import syntax, { - QUERY_COMMANDS, AGGREGATION_FUNCTIONS_STATS, - STRING_FUNCTIONS, - DATETIME_FUNCTIONS, - IP_FUNCTIONS, BOOLEAN_FUNCTIONS, - NUMERIC_OPERATORS, + DATETIME_FUNCTIONS, FIELD_AND_FILTER_FUNCTIONS, + IP_FUNCTIONS, + NUMERIC_OPERATORS, + QUERY_COMMANDS, + STRING_FUNCTIONS, } from './syntax'; // Types -import { CloudWatchQuery } from './types'; -import { AbsoluteTimeRange, LanguageProvider, HistoryItem } from '@grafana/data'; +import { CloudWatchQuery, TSDBResponse } from './types'; +import { AbsoluteTimeRange, HistoryItem, LanguageProvider } from '@grafana/data'; import { CloudWatchDatasource } from './datasource'; -import { TypeaheadInput, TypeaheadOutput, Token } from '@grafana/ui'; +import { Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui'; import Prism, { Grammar } from 'prismjs'; export type CloudWatchHistoryItem = HistoryItem; @@ -49,8 +49,8 @@ export class CloudWatchLanguageProvider extends LanguageProvider { return syntax; } - request = (url: string, params?: any): Promise<{ data: { data: string[] } }> => { - return this.datasource.awsRequest(url, params); + request = (url: string, params?: any): Promise => { + return this.datasource.awsRequest(url, params).toPromise(); }; start = () => { diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index b73a478a0c9..c7a321c7f17 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -1,6 +1,4 @@ -import '../datasource'; -import { CloudWatchDatasource, MAX_ATTEMPTS } from '../datasource'; -import * as redux from 'app/store/store'; +import { interval, of, throwError } from 'rxjs'; import { DataFrame, DataQueryErrorType, @@ -8,22 +6,20 @@ import { DataSourceInstanceSettings, dateMath, getFrameDisplayName, + observableTester, } from '@grafana/data'; +import { FetchResponse } from '@grafana/runtime'; + +import * as redux from 'app/store/store'; +import '../datasource'; +import { CloudWatchDatasource, MAX_ATTEMPTS } from '../datasource'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { - CloudWatchLogsQuery, - CloudWatchLogsQueryStatus, - CloudWatchMetricsQuery, - CloudWatchQuery, - LogAction, -} from '../types'; +import { CloudWatchLogsQuery, CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, LogAction } from '../types'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState'; import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies'; -import { interval, of } from 'rxjs'; import { CustomVariableModel, initialVariableModelState, VariableHide } from '../../../../features/variables/types'; -import { TimeSrvStub } from '../../../../../test/specs/helpers'; import * as rxjsUtils from '../utils/rxjs/increasingInterval'; @@ -38,18 +34,22 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); -describe('CloudWatchDatasource', () => { - const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); +type Args = { response?: any; throws?: boolean; templateSrv?: TemplateSrv }; + +function getTestContext({ response = {}, throws = false, templateSrv = new TemplateSrv() }: Args = {}) { + jest.clearAllMocks(); + + const fetchMock = jest.spyOn(backendSrv, 'fetch'); + + throws + ? fetchMock.mockImplementation(() => throwError(response)) + : fetchMock.mockImplementation(() => of(createFetchResponse(response))); const instanceSettings = { jsonData: { defaultRegion: 'us-east-1' }, name: 'TestDatasource', } as DataSourceInstanceSettings; - let templateSrv = new TemplateSrv(); - const start = 1483196400 * 1000; - const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) }; - const timeSrv = { time: { from: '2016-12-31 15:00:00Z', to: '2016-12-31 16:00:00Z' }, timeRange: () => { @@ -60,35 +60,32 @@ describe('CloudWatchDatasource', () => { }, } as TimeSrv; - const ctx = { - templateSrv, - } as any; + const ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); + + return { ds, fetchMock, instanceSettings }; +} + +describe('CloudWatchDatasource', () => { + const start = 1483196400 * 1000; + const defaultTimeRange = { from: new Date(start), to: new Date(start + 3600 * 1000) }; beforeEach(() => { - ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); jest.clearAllMocks(); }); describe('When getting log groups', () => { - beforeEach(() => { - datasourceRequestMock.mockImplementation(() => - Promise.resolve({ - data: { - results: { - A: { - dataframes: [ - 'QVJST1cxAAD/////GAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFgAAAACAAAAKAAAAAQAAAB8////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAJz///8IAAAAFAAAAAkAAABsb2dHcm91cHMAAAAEAAAAbmFtZQAAAAABAAAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABMAAAAUAAAAAAABQFMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAGAAAAAwAAABsb2dHcm91cE5hbWUAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAD/////mAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAAGAGAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAEgAAAAhAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiAAAAAAAAACIAAAAAAAAANgFAAAAAAAAAAAAAAEAAAAhAAAAAAAAAAAAAAAAAAAAAAAAADIAAABiAAAAkQAAALwAAADuAAAAHwEAAFQBAACHAQAAtQEAAOoBAAAbAgAASgIAAHQCAAClAgAA1QIAABADAABEAwAAdgMAAKMDAADXAwAACQQAAEAEAAB3BAAAlwQAAK0EAAC8BAAA+wQAAEIFAABhBQAAeAUAAJIFAAC0BQAA1gUAAC9hd3MvY29udGFpbmVyaW5zaWdodHMvZGV2MzAzLXdvcmtzaG9wL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZGF0YXBsYW5lL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZmxvd2xvZ3MvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvYXBwbGljYXRpb24vYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL3BlcmZvcm1hbmNlL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcHJvZC11cy1lYXN0LTEvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tc3RhZ2luZy9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvYnVnYmFzaC1lYzIvcGVyZm9ybWFuY2UvYXdzL2Vjcy9jb250YWluZXJpbnNpZ2h0cy9lY3MtZGVtb3dvcmtzaG9wL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvZWNzLXdvcmtzaG9wLWRldi9wZXJmb3JtYW5jZS9hd3MvZWtzL2RldjMwMy13b3Jrc2hvcC9jbHVzdGVyL2F3cy9ldmVudHMvY2xvdWR0cmFpbC9hd3MvZXZlbnRzL2Vjcy9hd3MvbGFtYmRhL2N3c3luLW15Y2FuYXJ5LWZhYzk3ZGVkLWYxMzQtNDk5YS05ZDcxLTRjM2JlMWY2MzE4Mi9hd3MvbGFtYmRhL2N3c3luLXdhdGNoLWxpbmtjaGVja3MtZWY3ZWYyNzMtNWRhMi00NjYzLWFmNTQtZDJmNTJkNTViMDYwL2Vjcy9lY3MtY3dhZ2VudC1kYWVtb24tc2VydmljZS9lY3MvZWNzLWRlbW8tbGltaXRUYXNrQ2xvdWRUcmFpbC9EZWZhdWx0TG9nR3JvdXBjb250YWluZXItaW5zaWdodHMtcHJvbWV0aGV1cy1iZXRhY29udGFpbmVyLWluc2lnaHRzLXByb21ldGhldXMtZGVtbwAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAACgBAAAAAAAAoAAAAAAAAABgBgAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAWAAAAAIAAAAoAAAABAAAAHz///8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAnP///wgAAAAUAAAACQAAAGxvZ0dyb3VwcwAAAAQAAABuYW1lAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAEwAAABQAAAAAAAFAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAYAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAMAAAAbG9nR3JvdXBOYW1lAAAAAEgBAABBUlJPVzE=', - ], - refId: 'A', - }, - }, - }, - }) - ); - }); - it('should return log groups as an array of strings', async () => { - const logGroups = await ctx.ds.describeLogGroups(); + const response = { + results: { + A: { + dataframes: [ + 'QVJST1cxAAD/////GAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFgAAAACAAAAKAAAAAQAAAB8////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAJz///8IAAAAFAAAAAkAAABsb2dHcm91cHMAAAAEAAAAbmFtZQAAAAABAAAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABMAAAAUAAAAAAABQFMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAGAAAAAwAAABsb2dHcm91cE5hbWUAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAD/////mAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAAGAGAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAEgAAAAhAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiAAAAAAAAACIAAAAAAAAANgFAAAAAAAAAAAAAAEAAAAhAAAAAAAAAAAAAAAAAAAAAAAAADIAAABiAAAAkQAAALwAAADuAAAAHwEAAFQBAACHAQAAtQEAAOoBAAAbAgAASgIAAHQCAAClAgAA1QIAABADAABEAwAAdgMAAKMDAADXAwAACQQAAEAEAAB3BAAAlwQAAK0EAAC8BAAA+wQAAEIFAABhBQAAeAUAAJIFAAC0BQAA1gUAAC9hd3MvY29udGFpbmVyaW5zaWdodHMvZGV2MzAzLXdvcmtzaG9wL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZGF0YXBsYW5lL2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvZmxvd2xvZ3MvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9kZXYzMDMtd29ya3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2RldjMwMy13b3Jrc2hvcC9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvYXBwbGljYXRpb24vYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL2Vjb21tZXJjZS1zb2Nrc2hvcC9ob3N0L2F3cy9jb250YWluZXJpbnNpZ2h0cy9lY29tbWVyY2Utc29ja3Nob3AvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1wZXJmL3BlcmZvcm1hbmNlL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcGVyZi9wcm9tZXRoZXVzL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tcHJvZC11cy1lYXN0LTEvcGVyZm9ybWFuY2UvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2FwcGxpY2F0aW9uL2F3cy9jb250YWluZXJpbnNpZ2h0cy93YXRjaGRlbW8tc3RhZ2luZy9kYXRhcGxhbmUvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL2hvc3QvYXdzL2NvbnRhaW5lcmluc2lnaHRzL3dhdGNoZGVtby1zdGFnaW5nL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvYnVnYmFzaC1lYzIvcGVyZm9ybWFuY2UvYXdzL2Vjcy9jb250YWluZXJpbnNpZ2h0cy9lY3MtZGVtb3dvcmtzaG9wL3BlcmZvcm1hbmNlL2F3cy9lY3MvY29udGFpbmVyaW5zaWdodHMvZWNzLXdvcmtzaG9wLWRldi9wZXJmb3JtYW5jZS9hd3MvZWtzL2RldjMwMy13b3Jrc2hvcC9jbHVzdGVyL2F3cy9ldmVudHMvY2xvdWR0cmFpbC9hd3MvZXZlbnRzL2Vjcy9hd3MvbGFtYmRhL2N3c3luLW15Y2FuYXJ5LWZhYzk3ZGVkLWYxMzQtNDk5YS05ZDcxLTRjM2JlMWY2MzE4Mi9hd3MvbGFtYmRhL2N3c3luLXdhdGNoLWxpbmtjaGVja3MtZWY3ZWYyNzMtNWRhMi00NjYzLWFmNTQtZDJmNTJkNTViMDYwL2Vjcy9lY3MtY3dhZ2VudC1kYWVtb24tc2VydmljZS9lY3MvZWNzLWRlbW8tbGltaXRUYXNrQ2xvdWRUcmFpbC9EZWZhdWx0TG9nR3JvdXBjb250YWluZXItaW5zaWdodHMtcHJvbWV0aGV1cy1iZXRhY29udGFpbmVyLWluc2lnaHRzLXByb21ldGhldXMtZGVtbwAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAACgBAAAAAAAAoAAAAAAAAABgBgAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAWAAAAAIAAAAoAAAABAAAAHz///8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAnP///wgAAAAUAAAACQAAAGxvZ0dyb3VwcwAAAAQAAABuYW1lAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAAEwAAABQAAAAAAAFAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAYAAAADAAAAGxvZ0dyb3VwTmFtZQAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAMAAAAbG9nR3JvdXBOYW1lAAAAAEgBAABBUlJPVzE=', + ], + refId: 'A', + }, + }, + }; + const { ds } = getTestContext({ response }); const expectedLogGroups = [ '/aws/containerinsights/dev303-workshop/application', '/aws/containerinsights/dev303-workshop/dataplane', @@ -124,6 +121,9 @@ describe('CloudWatchDatasource', () => { 'container-insights-prometheus-beta', 'container-insights-prometheus-demo', ]; + + const logGroups = await ds.describeLogGroups({}); + expect(logGroups).toEqual(expectedLogGroups); }); }); @@ -134,6 +134,7 @@ describe('CloudWatchDatasource', () => { }); it('should add data links to response', () => { + const { ds } = getTestContext(); const mockResponse: DataQueryResponse = { data: [ { @@ -149,7 +150,7 @@ describe('CloudWatchDatasource', () => { ], }; - const mockOptions = { + const mockOptions: any = { targets: [ { refId: 'A', @@ -160,7 +161,7 @@ describe('CloudWatchDatasource', () => { ], }; - const saturatedResponse = ctx.ds.addDataLinksToLogsResponse(mockResponse, mockOptions); + const saturatedResponse = ds['addDataLinksToLogsResponse'](mockResponse, mockOptions); expect(saturatedResponse).toMatchObject({ data: [ { @@ -185,6 +186,7 @@ describe('CloudWatchDatasource', () => { }); it('should stop querying when no more data received a number of times in a row', async () => { + const { ds } = getTestContext(); const fakeFrames = genMockFrames(20); const initialRecordsMatched = fakeFrames[0].meta!.stats!.find(stat => stat.displayName === 'Records scanned')! .value!; @@ -209,7 +211,7 @@ describe('CloudWatchDatasource', () => { } let i = 0; - jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { + jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { if (subtype === 'GetQueryResults') { const mockObservable = of([fakeFrames[i]]); i++; @@ -219,7 +221,7 @@ describe('CloudWatchDatasource', () => { } }); - const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise(); + const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise(); const expectedData = [ { @@ -246,10 +248,11 @@ describe('CloudWatchDatasource', () => { }); it('should continue querying as long as new data is being received', async () => { + const { ds } = getTestContext(); const fakeFrames = genMockFrames(15); let i = 0; - jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { + jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { if (subtype === 'GetQueryResults') { const mockObservable = of([fakeFrames[i]]); i++; @@ -259,7 +262,7 @@ describe('CloudWatchDatasource', () => { } }); - const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise(); + const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise(); expect(myResponse).toEqual({ data: [fakeFrames[fakeFrames.length - 1]], key: 'test-key', @@ -269,9 +272,10 @@ describe('CloudWatchDatasource', () => { }); it('should stop querying when results come back with status "Complete"', async () => { + const { ds } = getTestContext(); const fakeFrames = genMockFrames(3); let i = 0; - jest.spyOn(ctx.ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { + jest.spyOn(ds, 'makeLogActionRequest').mockImplementation((subtype: LogAction) => { if (subtype === 'GetQueryResults') { const mockObservable = of([fakeFrames[i]]); i++; @@ -281,7 +285,7 @@ describe('CloudWatchDatasource', () => { } }); - const myResponse = await ctx.ds.logsQuery([{ queryId: 'fake-query-id', region: 'default' }]).toPromise(); + const myResponse = await ds.logsQuery([{ queryId: 'fake-query-id', region: 'default', refId: 'A' }]).toPromise(); expect(myResponse).toEqual({ data: [fakeFrames[2]], @@ -292,8 +296,9 @@ describe('CloudWatchDatasource', () => { }); it('should call the replace method on provided log groups', () => { - const replaceSpy = jest.spyOn(ctx.ds, 'replace').mockImplementation((target: string) => target); - ctx.ds.makeLogActionRequest('StartQuery', [ + const { ds } = getTestContext(); + const replaceSpy = jest.spyOn(ds, 'replace').mockImplementation((target: string) => target); + ds.makeLogActionRequest('StartQuery', [ { queryString: 'test query string', region: 'default', @@ -308,7 +313,7 @@ describe('CloudWatchDatasource', () => { }); describe('When performing CloudWatch metrics query', () => { - const query = { + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -353,28 +358,29 @@ describe('CloudWatchDatasource', () => { }, }; - beforeEach(() => { - datasourceRequestMock.mockImplementation(() => { - return Promise.resolve({ data: response }); + it('should generate the correct query', done => { + const { ds, fetchMock } = getTestContext({ response }); + + observableTester().subscribeAndExpectOnComplete({ + observable: ds.query(query), + expect: () => { + expect(fetchMock.mock.calls[0][0].data.queries).toMatchObject( + expect.arrayContaining([ + expect.objectContaining({ + namespace: query.targets[0].namespace, + metricName: query.targets[0].metricName, + dimensions: { InstanceId: ['i-12345678'] }, + statistics: query.targets[0].statistics, + period: query.targets[0].period, + }), + ]) + ); + }, + done, }); }); - it('should generate the correct query', async () => { - await ctx.ds.query(query).toPromise(); - expect(datasourceRequestMock.mock.calls[0][0].data.queries).toMatchObject( - expect.arrayContaining([ - expect.objectContaining({ - namespace: query.targets[0].namespace, - metricName: query.targets[0].metricName, - dimensions: { InstanceId: ['i-12345678'] }, - statistics: query.targets[0].statistics, - period: query.targets[0].period, - }), - ]) - ); - }); - - it('should generate the correct query with interval variable', async () => { + it('should generate the correct query with interval variable', done => { const period: CustomVariableModel = { ...initialVariableModelState, id: 'period', @@ -388,9 +394,10 @@ describe('CloudWatchDatasource', () => { hide: VariableHide.dontHide, type: 'custom', }; + const templateSrv = new TemplateSrv(); templateSrv.init([period]); - const query = { + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -409,11 +416,19 @@ describe('CloudWatchDatasource', () => { ], }; - await ctx.ds.query(query).toPromise(); - expect(datasourceRequestMock.mock.calls[0][0].data.queries[0].period).toEqual('600'); + const { ds, fetchMock } = getTestContext({ response, templateSrv }); + + observableTester().subscribeAndExpectOnComplete({ + observable: ds.query(query), + expect: () => { + expect(fetchMock.mock.calls[0][0].data.queries[0].period).toEqual('600'); + }, + done, + }); }); it.each(['pNN.NN', 'p9', 'p99.', 'p99.999'])('should cancel query for invalid extended statistics (%s)', stat => { + const { ds } = getTestContext({ response }); const query = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, @@ -432,85 +447,94 @@ describe('CloudWatchDatasource', () => { }, ], }; - expect(ctx.ds.query.bind(ctx.ds, query)).toThrow(/Invalid extended statistics/); + + expect(ds.query.bind(ds, query)).toThrow(/Invalid extended statistics/); }); it('should return series list', done => { - ctx.ds - .query(query) - .toPromise() - .then((result: any) => { + const { ds } = getTestContext({ response }); + + observableTester().subscribeAndExpectOnNext({ + observable: ds.query(query), + expect: result => { expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]); - done(); - }); + }, + done, + }); }); describe('a correct cloudwatch url should be built for each time series in the response', () => { - beforeEach(() => { - datasourceRequestMock.mockImplementation(() => { - return Promise.resolve({ data: response }); - }); - }); - it('should be built correctly if theres one search expressions returned in meta for a given query row', done => { + const { ds } = getTestContext({ response }); + response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }]; - ctx.ds - .query(query) - .toPromise() - .then((result: any) => { + + observableTester().subscribeAndExpectOnNext({ + observable: ds.query(query), + expect: result => { expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console'); expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain( `region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}` ); - done(); - }); + }, + done, + }); }); it('should be built correctly if theres two search expressions returned in meta for a given query row', done => { + const { ds } = getTestContext({ response }); + response.results['A'].meta.gmdMeta = [ { Expression: `REMOVE_EMPTY(SEARCH('first expression'))` }, { Expression: `REMOVE_EMPTY(SEARCH('second expression'))` }, ]; - ctx.ds - .query(query) - .toPromise() - .then((result: any) => { + + observableTester().subscribeAndExpectOnNext({ + observable: ds.query(query), + expect: result => { expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console'); expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain( `region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}` ); - done(); - }); + }, + done, + }); }); it('should be built correctly if the query is a metric stat query', done => { + const { ds } = getTestContext({ response }); + response.results['A'].meta.gmdMeta = [{ Period: '300' }]; - ctx.ds - .query(query) - .toPromise() - .then((result: any) => { + + observableTester().subscribeAndExpectOnNext({ + observable: ds.query(query), + expect: result => { expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console'); expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain( `region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}` ); - done(); - }); + }, + done, + }); }); it('should not be added at all if query is a math expression', done => { + const { ds } = getTestContext({ response }); + query.targets[0].expression = 'a * 2'; response.results['A'].meta.searchExpressions = []; - ctx.ds - .query(query) - .toPromise() - .then((result: any) => { + + observableTester().subscribeAndExpectOnNext({ + observable: ds.query(query), + expect: result => { expect(result.data[0].fields[1].config.links).toBeUndefined(); - done(); - }); + }, + done, + }); }); }); @@ -527,7 +551,7 @@ describe('CloudWatchDatasource', () => { expression: '', }; - const query = { + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -576,55 +600,50 @@ describe('CloudWatchDatasource', () => { redux.setStore({ dispatch: jest.fn(), } as any); - - datasourceRequestMock.mockImplementation(() => { - return Promise.reject(backendErrorResponse); - }); }); it('should display one alert error message per region+datasource combination', done => { - const memoizedDebounceSpy = jest.spyOn(ctx.ds, 'debouncedAlert'); - ctx.ds - .query(query) - .toPromise() - .catch(() => { + const { ds } = getTestContext({ response: backendErrorResponse, throws: true }); + const memoizedDebounceSpy = jest.spyOn(ds, 'debouncedAlert'); + + observableTester().subscribeAndExpectOnError({ + observable: ds.query(query), + expect: err => { expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-1'); expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'us-east-2'); expect(memoizedDebounceSpy).toHaveBeenCalledWith('TestDatasource', 'eu-north-1'); expect(memoizedDebounceSpy).toBeCalledTimes(3); - done(); - }); + }, + done, + }); }); }); describe('when regions query is used', () => { - beforeEach(() => { - datasourceRequestMock.mockImplementation(() => { - return Promise.resolve({}); - }); - ctx.ds = new CloudWatchDatasource(instanceSettings, templateSrv, timeSrv); - ctx.ds.doMetricQueryRequest = jest.fn(() => []); - }); describe('and region param is left out', () => { - it('should use the default region', done => { - ctx.ds.metricFindQuery('metrics(testNamespace)').then(() => { - expect(ctx.ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { - namespace: 'testNamespace', - region: instanceSettings.jsonData.defaultRegion, - }); - done(); + it('should use the default region', async () => { + const { ds, instanceSettings } = getTestContext(); + ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]); + + await ds.metricFindQuery('metrics(testNamespace)'); + + expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { + namespace: 'testNamespace', + region: instanceSettings.jsonData.defaultRegion, }); }); }); describe('and region param is defined by user', () => { - it('should use the user defined region', done => { - ctx.ds.metricFindQuery('metrics(testNamespace2, custom-region)').then(() => { - expect(ctx.ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { - namespace: 'testNamespace2', - region: 'custom-region', - }); - done(); + it('should use the user defined region', async () => { + const { ds } = getTestContext(); + ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]); + + await ds.metricFindQuery('metrics(testNamespace2, custom-region)'); + + expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { + namespace: 'testNamespace2', + region: 'custom-region', }); }); }); @@ -633,27 +652,25 @@ describe('CloudWatchDatasource', () => { describe('When query region is "default"', () => { it('should return the datasource region if empty or "default"', () => { + const { ds, instanceSettings } = getTestContext(); const defaultRegion = instanceSettings.jsonData.defaultRegion; - expect(ctx.ds.getActualRegion()).toBe(defaultRegion); - expect(ctx.ds.getActualRegion('')).toBe(defaultRegion); - expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion); + expect(ds.getActualRegion()).toBe(defaultRegion); + expect(ds.getActualRegion('')).toBe(defaultRegion); + expect(ds.getActualRegion('default')).toBe(defaultRegion); }); it('should return the specified region if specified', () => { - expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); - }); + const { ds } = getTestContext(); - let requestParams: { queries: CloudWatchQuery[] }; - beforeEach(() => { - ctx.ds.performTimeSeriesQuery = jest.fn(request => { - requestParams = request; - return Promise.resolve({ data: {} }); - }); + expect(ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1'); }); it('should query for the datasource region if empty or "default"', done => { - const query = { + const { ds, instanceSettings } = getTestContext(); + const performTimeSeriesQueryMock = jest.spyOn(ds, 'performTimeSeriesQuery').mockReturnValue(of({})); + + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -672,36 +689,29 @@ describe('CloudWatchDatasource', () => { ], }; - ctx.ds - .query(query) - .toPromise() - .then((result: any) => { - expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion); - done(); - }); + observableTester().subscribeAndExpectOnComplete({ + observable: ds.query(query), + expect: () => { + expect(performTimeSeriesQueryMock.mock.calls[0][0].queries[0].region).toBe( + instanceSettings.jsonData.defaultRegion + ); + }, + done, + }); }); }); describe('When interpolating variables', () => { - beforeEach(() => { - jest.clearAllMocks(); - - ctx.mockedTemplateSrv = { - replace: jest.fn(), - }; - - ctx.ds = new CloudWatchDatasource( - instanceSettings, - ctx.mockedTemplateSrv, - (new TimeSrvStub() as unknown) as TimeSrv - ); - }); - it('should return an empty array if no queries are provided', () => { - expect(ctx.ds.interpolateVariablesInQueries([], {})).toHaveLength(0); + const templateSrv: any = { replace: jest.fn() }; + const { ds } = getTestContext({ templateSrv }); + + expect(ds.interpolateVariablesInQueries([], {})).toHaveLength(0); }); it('should replace correct variables in CloudWatchLogsQuery', () => { + const templateSrv: any = { replace: jest.fn() }; + const { ds } = getTestContext({ templateSrv }); const variableName = 'someVar'; const logQuery: CloudWatchLogsQuery = { id: 'someId', @@ -711,14 +721,16 @@ describe('CloudWatchDatasource', () => { region: `$${variableName}`, }; - ctx.ds.interpolateVariablesInQueries([logQuery], {}); + ds.interpolateVariablesInQueries([logQuery], {}); // We interpolate `expression` and `region` in CloudWatchLogsQuery - expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); - expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(2); + expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); + expect(templateSrv.replace).toHaveBeenCalledTimes(2); }); it('should replace correct variables in CloudWatchMetricsQuery', () => { + const templateSrv: any = { replace: jest.fn() }; + const { ds } = getTestContext({ templateSrv }); const variableName = 'someVar'; const logQuery: CloudWatchMetricsQuery = { id: 'someId', @@ -737,16 +749,16 @@ describe('CloudWatchDatasource', () => { statistics: [], }; - ctx.ds.interpolateVariablesInQueries([logQuery], {}); + ds.interpolateVariablesInQueries([logQuery], {}); // We interpolate `expression`, `region`, `period`, `alias`, `metricName`, `nameSpace` and `dimensions` in CloudWatchMetricsQuery - expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); - expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(8); + expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); + expect(templateSrv.replace).toHaveBeenCalledTimes(8); }); }); describe('When performing CloudWatch query for extended statistics', () => { - const query = { + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -797,26 +809,22 @@ describe('CloudWatchDatasource', () => { }, }; - beforeEach(() => { - datasourceRequestMock.mockImplementation(params => { - return Promise.resolve({ data: response }); - }); - }); - it('should return series list', done => { - ctx.ds - .query(query) - .toPromise() - .then((result: any) => { + const { ds } = getTestContext({ response }); + + observableTester().subscribeAndExpectOnNext({ + observable: ds.query(query), + expect: result => { expect(getFrameDisplayName(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]); - done(); - }); + }, + done, + }); }); }); describe('When performing CloudWatch query with template variables', () => { - let requestParams: { queries: CloudWatchMetricsQuery[] }; + let templateSrv: TemplateSrv; beforeEach(() => { const var1: CustomVariableModel = { ...initialVariableModelState, @@ -880,18 +888,13 @@ describe('CloudWatchDatasource', () => { }; const variables = [var1, var2, var3, var4]; const state = convertToStoreState(variables); - const _templateSrv = new TemplateSrv(getTemplateSrvDependencies(state)); - _templateSrv.init(variables); - ctx.ds = new CloudWatchDatasource(instanceSettings, _templateSrv, timeSrv); - - datasourceRequestMock.mockImplementation(params => { - requestParams = params.data; - return Promise.resolve({ data: {} }); - }); + templateSrv = new TemplateSrv(getTemplateSrvDependencies(state)); + templateSrv.init(variables); }); it('should generate the correct query for single template variable', done => { - const query = { + const { ds, fetchMock } = getTestContext({ templateSrv }); + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -910,17 +913,18 @@ describe('CloudWatchDatasource', () => { ], }; - ctx.ds - .query(query) - .toPromise() - .then(() => { - expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); - done(); - }); + observableTester().subscribeAndExpectOnComplete({ + observable: ds.query(query), + expect: () => { + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); + }, + done, + }); }); it('should generate the correct query in the case of one multilple template variables', done => { - const query = { + const { ds, fetchMock } = getTestContext({ templateSrv }); + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -945,19 +949,20 @@ describe('CloudWatchDatasource', () => { }, }; - ctx.ds - .query(query) - .toPromise() - .then(() => { - expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); - expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); - expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); - done(); - }); + observableTester().subscribeAndExpectOnComplete({ + observable: ds.query(query), + expect: () => { + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); + }, + done, + }); }); it('should generate the correct query in the case of multilple multi template variables', done => { - const query = { + const { ds, fetchMock } = getTestContext({ templateSrv }); + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -978,19 +983,20 @@ describe('CloudWatchDatasource', () => { ], }; - ctx.ds - .query(query) - .toPromise() - .then(() => { - expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); - expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); - expect(requestParams.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']); - done(); - }); + observableTester().subscribeAndExpectOnComplete({ + observable: ds.query(query), + expect: () => { + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim4']).toStrictEqual(['var4-foo', 'var4-baz']); + }, + done, + }); }); it('should generate the correct query for multilple template variables, lack scopedVars', done => { - const query = { + const { ds, fetchMock } = getTestContext({ templateSrv }); + const query: any = { range: defaultTimeRange, rangeRaw: { from: 1483228800, to: 1483232400 }, targets: [ @@ -1014,15 +1020,15 @@ describe('CloudWatchDatasource', () => { }, }; - ctx.ds - .query(query) - .toPromise() - .then(() => { - expect(requestParams.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); - expect(requestParams.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); - expect(requestParams.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); - done(); - }); + observableTester().subscribeAndExpectOnComplete({ + observable: ds.query(query), + expect: () => { + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim1']).toStrictEqual(['var1-foo']); + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim2']).toStrictEqual(['var2-foo']); + expect(fetchMock.mock.calls[0][0].data.queries[0].dimensions['dim3']).toStrictEqual(['var3-foo', 'var3-baz']); + }, + done, + }); }); }); @@ -1032,11 +1038,8 @@ describe('CloudWatchDatasource', () => { scenario.setup = async (setupCallback: any) => { beforeEach(async () => { await setupCallback(); - datasourceRequestMock.mockImplementation(args => { - scenario.request = args.data; - return Promise.resolve({ data: scenario.requestResponse }); - }); - ctx.ds.metricFindQuery(query).then((args: any) => { + const { ds } = getTestContext({ response: scenario.requestResponse }); + ds.metricFindQuery(query).then((args: any) => { scenario.result = args; }); }); @@ -1210,3 +1213,17 @@ function genMockFrames(numResponses: number): DataFrame[] { return mockFrames; } + +function createFetchResponse(data: T): FetchResponse { + return { + data, + status: 200, + url: 'http://localhost:3000/api/query', + config: { url: 'http://localhost:3000/api/query' }, + type: 'basic', + statusText: 'Ok', + redirected: false, + headers: ({} as unknown) as Headers, + ok: true, + }; +}