grafana/public/app/features/datasources/state/actions.test.ts
Piotr Jamróz 9ace8686a1
Datasources: Improve error handling for testing data sources (#35120)
* Improve error handling for error messages

The error message will be read from error object from the following properties in the following order:
- message
- data.message
- statusText

* Convert api/ds/query errors to TestingStatus

SQL datasources (mysql, mssql, postgres) and CloudWatch use api/ds/query to test the data source, but previously didn't handle errors returned by this endpoint. If the error cannot be handled it's re-thrown to be handled in public/app/features/datasources/state/actions.ts

* Use async/await instead of Promises

* Remove incorrect type import

TestingStatus is in app/types. Should be pulled down to grafana/data but it depends on HealthCheckResultDetails that is public and lives in grafana/runtime. Ideally TestingStatus should live in grafana/data but I'm not sure if HealthCheckResultDetails can be move there too (?)

* Update packages/grafana-data/src/types/datasource.ts

Co-authored-by: Erik Sundell <erik.sundell@grafana.com>

* Handle errors with no details in toTestingStatus instead of re-throwing

* Update packages/grafana-data/src/types/datasource.ts

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>

Co-authored-by: Erik Sundell <erik.sundell@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
2021-07-08 14:32:27 +02:00

255 lines
8.5 KiB
TypeScript

import {
findNewName,
nameExits,
InitDataSourceSettingDependencies,
testDataSource,
TestDataSourceDependencies,
} from './actions';
import { getMockPlugin, getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
import { thunkTester } from 'test/core/thunk/thunkTester';
import {
initDataSourceSettingsSucceeded,
initDataSourceSettingsFailed,
testDataSourceStarting,
testDataSourceSucceeded,
testDataSourceFailed,
} from './reducers';
import { initDataSourceSettings } from '../state/actions';
import { ThunkResult, ThunkDispatch } from 'app/types';
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
const getBackendSrvMock = () =>
({
get: jest.fn().mockReturnValue({
testDatasource: jest.fn().mockReturnValue({
status: '',
message: '',
}),
}),
withNoBackendCache: jest.fn().mockImplementationOnce((cb) => cb()),
} as any);
const failDataSourceTest = async (error: object) => {
const dependencies: TestDataSourceDependencies = {
getDatasourceSrv: () =>
({
get: jest.fn().mockReturnValue({
testDatasource: jest.fn().mockImplementation(() => {
throw error;
}),
}),
} as any),
getBackendSrv: getBackendSrvMock,
};
const state = {
testingStatus: {
message: '',
status: '',
},
};
const dispatchedActions = await thunkTester(state)
.givenThunk(testDataSource)
.whenThunkIsDispatched('Azure Monitor', dependencies);
return dispatchedActions;
};
describe('Name exists', () => {
const plugins = getMockPlugins(5);
it('should be true', () => {
const name = 'pretty cool plugin-1';
expect(nameExits(plugins, name)).toEqual(true);
});
it('should be false', () => {
const name = 'pretty cool plugin-6';
expect(nameExits(plugins, name));
});
});
describe('Find new name', () => {
it('should create a new name', () => {
const plugins = getMockPlugins(5);
const name = 'pretty cool plugin-1';
expect(findNewName(plugins, name)).toEqual('pretty cool plugin-6');
});
it('should create new name without suffix', () => {
const plugin = getMockPlugin();
plugin.name = 'prometheus';
const plugins = [plugin];
const name = 'prometheus';
expect(findNewName(plugins, name)).toEqual('prometheus-1');
});
it('should handle names that end with -', () => {
const plugin = getMockPlugin();
const plugins = [plugin];
const name = 'pretty cool plugin-';
expect(findNewName(plugins, name)).toEqual('pretty cool plugin-');
});
});
describe('initDataSourceSettings', () => {
describe('when pageId is missing', () => {
it('then initDataSourceSettingsFailed should be dispatched', async () => {
const dispatchedActions = await thunkTester({}).givenThunk(initDataSourceSettings).whenThunkIsDispatched('');
expect(dispatchedActions).toEqual([initDataSourceSettingsFailed(new Error('Invalid ID'))]);
});
});
describe('when pageId is a valid', () => {
it('then initDataSourceSettingsSucceeded should be dispatched', async () => {
const thunkMock = (): ThunkResult<void> => (dispatch: ThunkDispatch, getState) => {};
const dataSource = { type: 'app' };
const dataSourceMeta = { id: 'some id' };
const dependencies: InitDataSourceSettingDependencies = {
loadDataSource: jest.fn(thunkMock) as any,
getDataSource: jest.fn().mockReturnValue(dataSource),
getDataSourceMeta: jest.fn().mockReturnValue(dataSourceMeta),
importDataSourcePlugin: jest.fn().mockReturnValue({} as GenericDataSourcePlugin),
};
const state = {
dataSourceSettings: {},
dataSources: {},
};
const dispatchedActions = await thunkTester(state)
.givenThunk(initDataSourceSettings)
.whenThunkIsDispatched(256, dependencies);
expect(dispatchedActions).toEqual([initDataSourceSettingsSucceeded({} as GenericDataSourcePlugin)]);
expect(dependencies.loadDataSource).toHaveBeenCalledTimes(1);
expect(dependencies.loadDataSource).toHaveBeenCalledWith(256);
expect(dependencies.getDataSource).toHaveBeenCalledTimes(1);
expect(dependencies.getDataSource).toHaveBeenCalledWith({}, 256);
expect(dependencies.getDataSourceMeta).toHaveBeenCalledTimes(1);
expect(dependencies.getDataSourceMeta).toHaveBeenCalledWith({}, 'app');
expect(dependencies.importDataSourcePlugin).toHaveBeenCalledTimes(1);
expect(dependencies.importDataSourcePlugin).toHaveBeenCalledWith(dataSourceMeta);
});
});
describe('when plugin loading fails', () => {
it('then initDataSourceSettingsFailed should be dispatched', async () => {
const dependencies: InitDataSourceSettingDependencies = {
loadDataSource: jest.fn().mockImplementation(() => {
throw new Error('Error loading plugin');
}),
getDataSource: jest.fn(),
getDataSourceMeta: jest.fn(),
importDataSourcePlugin: jest.fn(),
};
const state = {
dataSourceSettings: {},
dataSources: {},
};
const dispatchedActions = await thunkTester(state)
.givenThunk(initDataSourceSettings)
.whenThunkIsDispatched(301, dependencies);
expect(dispatchedActions).toEqual([initDataSourceSettingsFailed(new Error('Error loading plugin'))]);
expect(dependencies.loadDataSource).toHaveBeenCalledTimes(1);
expect(dependencies.loadDataSource).toHaveBeenCalledWith(301);
});
});
});
describe('testDataSource', () => {
describe('when a datasource is tested', () => {
it('then testDataSourceStarting and testDataSourceSucceeded should be dispatched', async () => {
const dependencies: TestDataSourceDependencies = {
getDatasourceSrv: () =>
({
get: jest.fn().mockReturnValue({
testDatasource: jest.fn().mockReturnValue({
status: '',
message: '',
}),
}),
} as any),
getBackendSrv: getBackendSrvMock,
};
const state = {
testingStatus: {
status: '',
message: '',
},
};
const dispatchedActions = await thunkTester(state)
.givenThunk(testDataSource)
.whenThunkIsDispatched('Azure Monitor', dependencies);
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceSucceeded(state.testingStatus)]);
});
it('then testDataSourceFailed should be dispatched', async () => {
const dependencies: TestDataSourceDependencies = {
getDatasourceSrv: () =>
({
get: jest.fn().mockReturnValue({
testDatasource: jest.fn().mockImplementation(() => {
throw new Error('Error testing datasource');
}),
}),
} as any),
getBackendSrv: getBackendSrvMock,
};
const result = {
message: 'Error testing datasource',
};
const state = {
testingStatus: {
message: '',
status: '',
},
};
const dispatchedActions = await thunkTester(state)
.givenThunk(testDataSource)
.whenThunkIsDispatched('Azure Monitor', dependencies);
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
});
it('then testDataSourceFailed should be dispatched with response error message', async () => {
const result = {
message: 'Error testing datasource',
};
const dispatchedActions = await failDataSourceTest({
message: 'Error testing datasource',
data: { message: 'Response error message' },
statusText: 'Bad Request',
});
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
});
it('then testDataSourceFailed should be dispatched with response data message', async () => {
const result = {
message: 'Response error message',
};
const dispatchedActions = await failDataSourceTest({
data: { message: 'Response error message' },
statusText: 'Bad Request',
});
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
});
it('then testDataSourceFailed should be dispatched with response statusText', async () => {
const result = {
message: 'HTTP error Bad Request',
};
const dispatchedActions = await failDataSourceTest({ data: {}, statusText: 'Bad Request' });
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
});
});
});