mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Metering connector actions request body bytes (#186804)
Towards: https://github.com/elastic/response-ops-team/issues/209 This PR collects body-bytes from the requests made by the connectors to the 3rd parties and saves them in the event-log. There is a new metric collector: `ConnectorMetricsCollector` Action TaskRunner, creates a new instance of it on each action execution and passes it to the actionType.executor. Then the actionType.executor passes it to the request function provided by the actions plugin. Request function passes the response (or the error) from axios to `addRequestBodyBytes` method of the `ConnectorMetricsCollector`. Since axios always returns `request.headers['Content-Length']` either in success result or error, metric collector uses its value to get the request body bytes. In case there is no `Content-Length` header, `addRequestBodyBytes` method fallbacks to the body object that we pass as the second param. So It calculates the body bytes by using `Buffer.byteLength(body, 'utf8');`, which is also used by axios to populate `request.headers['Content-Length']` For the connectors or the subActions that we don't use the request function or axios: addRequestBodyBytes method is called just before making the request only with the body param in order to force it to use the fallback. Note: If there are more than one requests in an execution, the bytes are summed. ## To verify: Create a rule with a connector that you would like to test. Let the rule run and check the event-log of your connector, request body bytes should be saved in: `kibana.action.execution.metrics.request_body_bytes` Alternatively: You can create a connector and run it on its test tab. You can use the below query to check the event-log: ``` { "query": { "bool": { "must": [ { "match": { "event.provider":"actions"}}, { "match": { "kibana.action.type_id":"{**your-action-type-id**}"}} ], "filter": [ { "term": { "event.action": "execute" }} ] } }, "size" : 100 } ```
This commit is contained in:
parent
8431033910
commit
9372027e6c
114 changed files with 4348 additions and 1691 deletions
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ActionExecutor } from './action_executor';
|
||||
|
@ -18,7 +17,7 @@ import {
|
|||
} from '@kbn/core/server/mocks';
|
||||
import { eventLoggerMock } from '@kbn/event-log-plugin/server/mocks';
|
||||
import { spacesServiceMock } from '@kbn/spaces-plugin/server/spaces_service/spaces_service.mock';
|
||||
import { ActionType as ConnectorType } from '../types';
|
||||
import { ActionType as ConnectorType, ConnectorUsageCollector } from '../types';
|
||||
import { actionsAuthorizationMock, actionsMock } from '../mocks';
|
||||
import {
|
||||
asBackgroundTaskExecutionSource,
|
||||
|
@ -150,6 +149,10 @@ const connectorSavedObject = {
|
|||
references: [],
|
||||
};
|
||||
|
||||
interface ActionUsage {
|
||||
request_body_bytes: number;
|
||||
}
|
||||
|
||||
const getBaseExecuteStartEventLogDoc = (unsecured: boolean) => {
|
||||
return {
|
||||
event: {
|
||||
|
@ -163,6 +166,7 @@ const getBaseExecuteStartEventLogDoc = (unsecured: boolean) => {
|
|||
},
|
||||
id: CONNECTOR_ID,
|
||||
name: '1',
|
||||
type_id: 'test',
|
||||
},
|
||||
...(unsecured
|
||||
? {}
|
||||
|
@ -190,10 +194,23 @@ const getBaseExecuteStartEventLogDoc = (unsecured: boolean) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getBaseExecuteEventLogDoc = (unsecured: boolean) => {
|
||||
const getBaseExecuteEventLogDoc = (
|
||||
unsecured: boolean,
|
||||
actionUsage: ActionUsage = { request_body_bytes: 0 }
|
||||
) => {
|
||||
const base = getBaseExecuteStartEventLogDoc(unsecured);
|
||||
return {
|
||||
...base,
|
||||
kibana: {
|
||||
...base.kibana,
|
||||
action: {
|
||||
...base.kibana.action,
|
||||
execution: {
|
||||
...base.kibana.action.execution,
|
||||
usage: actionUsage,
|
||||
},
|
||||
},
|
||||
},
|
||||
event: {
|
||||
...base.event,
|
||||
action: 'execute',
|
||||
|
@ -211,9 +228,12 @@ const getBaseExecuteEventLogDoc = (unsecured: boolean) => {
|
|||
};
|
||||
};
|
||||
|
||||
const mockGetRequestBodyByte = jest.spyOn(ConnectorUsageCollector.prototype, 'getRequestBodyByte');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
mockGetRequestBodyByte.mockReturnValue(0);
|
||||
spacesMock.getSpaceId.mockReturnValue('some-namespace');
|
||||
loggerMock.get.mockImplementation(() => loggerMock);
|
||||
const mockRealm = { name: 'default_native', type: 'native' };
|
||||
|
@ -237,6 +257,7 @@ describe('Action Executor', () => {
|
|||
const label = executeUnsecure ? 'executes unsecured' : 'executes';
|
||||
|
||||
test(`successfully ${label}`, async () => {
|
||||
mockGetRequestBodyByte.mockReturnValue(300);
|
||||
encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(
|
||||
connectorSavedObject
|
||||
);
|
||||
|
@ -280,13 +301,15 @@ describe('Action Executor', () => {
|
|||
},
|
||||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1');
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(2);
|
||||
|
||||
const execStartDoc = getBaseExecuteStartEventLogDoc(executeUnsecure);
|
||||
const execDoc = getBaseExecuteEventLogDoc(executeUnsecure);
|
||||
const execDoc = getBaseExecuteEventLogDoc(executeUnsecure, { request_body_bytes: 300 });
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, execStartDoc);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, execDoc);
|
||||
});
|
||||
|
@ -353,6 +376,7 @@ describe('Action Executor', () => {
|
|||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
source: executionSource.source,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1');
|
||||
|
@ -360,6 +384,7 @@ describe('Action Executor', () => {
|
|||
|
||||
const execStartDoc = getBaseExecuteStartEventLogDoc(executeUnsecure);
|
||||
const execDoc = getBaseExecuteEventLogDoc(executeUnsecure);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, {
|
||||
...execStartDoc,
|
||||
kibana: {
|
||||
|
@ -431,6 +456,7 @@ describe('Action Executor', () => {
|
|||
},
|
||||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured');
|
||||
|
@ -438,6 +464,7 @@ describe('Action Executor', () => {
|
|||
|
||||
const execStartDoc = getBaseExecuteStartEventLogDoc(executeUnsecure);
|
||||
const execDoc = getBaseExecuteEventLogDoc(executeUnsecure);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, {
|
||||
...execStartDoc,
|
||||
kibana: {
|
||||
|
@ -513,6 +540,7 @@ describe('Action Executor', () => {
|
|||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
request: {},
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -532,6 +560,7 @@ describe('Action Executor', () => {
|
|||
|
||||
const execStartDoc = getBaseExecuteStartEventLogDoc(executeUnsecure);
|
||||
const execDoc = getBaseExecuteEventLogDoc(executeUnsecure);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, {
|
||||
...execStartDoc,
|
||||
kibana: {
|
||||
|
@ -540,6 +569,7 @@ describe('Action Executor', () => {
|
|||
...execStartDoc.kibana.action,
|
||||
id: 'system-connector-.cases',
|
||||
name: 'System action: .cases',
|
||||
type_id: '.cases',
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -569,6 +599,7 @@ describe('Action Executor', () => {
|
|||
...execDoc.kibana.action,
|
||||
id: 'system-connector-.cases',
|
||||
name: 'System action: .cases',
|
||||
type_id: '.cases',
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -890,6 +921,7 @@ describe('Action Executor', () => {
|
|||
},
|
||||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -921,6 +953,7 @@ describe('Action Executor', () => {
|
|||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
request: {},
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -989,6 +1022,7 @@ describe('Action Executor', () => {
|
|||
},
|
||||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(loggerMock.debug).toBeCalledWith('executing action test:preconfigured: Preconfigured');
|
||||
|
@ -996,6 +1030,7 @@ describe('Action Executor', () => {
|
|||
|
||||
const execStartDoc = getBaseExecuteStartEventLogDoc(executeUnsecure);
|
||||
const execDoc = getBaseExecuteEventLogDoc(executeUnsecure);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, {
|
||||
...execStartDoc,
|
||||
kibana: {
|
||||
|
@ -1026,6 +1061,12 @@ describe('Action Executor', () => {
|
|||
...execDoc.kibana.action,
|
||||
id: 'preconfigured',
|
||||
name: 'Preconfigured',
|
||||
execution: {
|
||||
...execStartDoc.kibana.action.execution,
|
||||
usage: {
|
||||
request_body_bytes: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -1074,6 +1115,7 @@ describe('Action Executor', () => {
|
|||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
request: {},
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(loggerMock.debug).toBeCalledWith(
|
||||
|
@ -1083,6 +1125,7 @@ describe('Action Executor', () => {
|
|||
|
||||
const execStartDoc = getBaseExecuteStartEventLogDoc(executeUnsecure);
|
||||
const execDoc = getBaseExecuteEventLogDoc(executeUnsecure);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, {
|
||||
...execStartDoc,
|
||||
kibana: {
|
||||
|
@ -1091,6 +1134,7 @@ describe('Action Executor', () => {
|
|||
...execStartDoc.kibana.action,
|
||||
id: 'system-connector-.cases',
|
||||
name: 'System action: .cases',
|
||||
type_id: '.cases',
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -1120,6 +1164,7 @@ describe('Action Executor', () => {
|
|||
...execDoc.kibana.action,
|
||||
id: 'system-connector-.cases',
|
||||
name: 'System action: .cases',
|
||||
type_id: '.cases',
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -1290,6 +1335,7 @@ describe('Action Executor', () => {
|
|||
},
|
||||
params: { foo: true },
|
||||
logger: loggerMock,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1385,6 +1431,7 @@ describe('Event log', () => {
|
|||
},
|
||||
name: undefined,
|
||||
id: 'action1',
|
||||
type_id: 'test',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
|
@ -1430,6 +1477,7 @@ describe('Event log', () => {
|
|||
},
|
||||
name: 'action-1',
|
||||
id: '1',
|
||||
type_id: 'test',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
|
@ -1483,6 +1531,7 @@ describe('Event log', () => {
|
|||
},
|
||||
name: 'action-1',
|
||||
id: '1',
|
||||
type_id: 'test',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
|
@ -1559,9 +1608,13 @@ describe('Event log', () => {
|
|||
gen_ai: {
|
||||
usage: mockGenAi.usage,
|
||||
},
|
||||
usage: {
|
||||
request_body_bytes: 0,
|
||||
},
|
||||
},
|
||||
name: 'action-1',
|
||||
id: '1',
|
||||
type_id: '.gen-ai',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
|
@ -1655,9 +1708,13 @@ describe('Event log', () => {
|
|||
total_tokens: 35,
|
||||
},
|
||||
},
|
||||
usage: {
|
||||
request_body_bytes: 0,
|
||||
},
|
||||
},
|
||||
name: 'action-1',
|
||||
id: '1',
|
||||
type_id: '.gen-ai',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
|
|
|
@ -23,6 +23,7 @@ import { IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '@kbn/event-log-plugin/se
|
|||
import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server';
|
||||
import { getErrorSource } from '@kbn/task-manager-plugin/server/task_running';
|
||||
import { GEN_AI_TOKEN_COUNT_EVENT } from './event_based_telemetry';
|
||||
import { ConnectorUsageCollector } from '../usage/connector_usage_collector';
|
||||
import { getGenAiTokenTracking, shouldTrackGenAiToken } from './gen_ai_token_tracking';
|
||||
import {
|
||||
validateConfig,
|
||||
|
@ -293,6 +294,7 @@ export class ActionExecutor {
|
|||
actionExecutionId,
|
||||
isInMemory: this.actionInfo.isInMemory,
|
||||
...(source ? { source } : {}),
|
||||
actionTypeId: this.actionInfo.actionTypeId,
|
||||
});
|
||||
|
||||
eventLogger.logEvent(event);
|
||||
|
@ -394,6 +396,14 @@ export class ActionExecutor {
|
|||
|
||||
const { actionTypeId, name, config, secrets } = actionInfo;
|
||||
|
||||
const loggerId = actionTypeId.startsWith('.') ? actionTypeId.substring(1) : actionTypeId;
|
||||
const logger = this.actionExecutorContext!.logger.get(loggerId);
|
||||
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: actionId,
|
||||
});
|
||||
|
||||
if (!this.actionInfo || this.actionInfo.actionId !== actionId) {
|
||||
this.actionInfo = actionInfo;
|
||||
}
|
||||
|
@ -434,9 +444,6 @@ export class ActionExecutor {
|
|||
return err.result;
|
||||
}
|
||||
|
||||
const loggerId = actionTypeId.startsWith('.') ? actionTypeId.substring(1) : actionTypeId;
|
||||
const logger = this.actionExecutorContext!.logger.get(loggerId);
|
||||
|
||||
if (span) {
|
||||
span.name = `${executeLabel} ${actionTypeId}`;
|
||||
span.addLabels({
|
||||
|
@ -477,6 +484,7 @@ export class ActionExecutor {
|
|||
actionExecutionId,
|
||||
isInMemory: this.actionInfo.isInMemory,
|
||||
...(source ? { source } : {}),
|
||||
actionTypeId,
|
||||
});
|
||||
|
||||
eventLogger.startTiming(event);
|
||||
|
@ -510,6 +518,7 @@ export class ActionExecutor {
|
|||
logger,
|
||||
source,
|
||||
...(actionType.isSystemActionType ? { request } : {}),
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
if (rawResult && rawResult.status === 'error') {
|
||||
|
@ -548,6 +557,11 @@ export class ActionExecutor {
|
|||
event.user = event.user || {};
|
||||
event.user.name = currentUser?.username;
|
||||
event.user.id = currentUser?.profile_uid;
|
||||
set(
|
||||
event,
|
||||
'kibana.action.execution.usage.request_body_bytes',
|
||||
connectorUsageCollector.getRequestBodyByte()
|
||||
);
|
||||
|
||||
if (result.status === 'ok') {
|
||||
span?.setOutcome('success');
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
||||
import { Agent as HttpsAgent } from 'https';
|
||||
import HttpProxyAgent from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
|
@ -21,6 +21,7 @@ import {
|
|||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { actionsConfigMock } from '../actions_config.mock';
|
||||
import { getCustomAgents } from './get_custom_agents';
|
||||
import { ConnectorUsageCollector } from '../usage/connector_usage_collector';
|
||||
|
||||
const TestUrl = 'https://elastic.co/foo/bar/baz';
|
||||
|
||||
|
@ -79,6 +80,80 @@ describe('request', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('adds request body bytes from request header on a successful request when connectorUsageCollector is provided', async () => {
|
||||
const contentLength = 12;
|
||||
axiosMock.mockImplementation(() => ({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
data: { incidentId: '123' },
|
||||
request: {
|
||||
headers: { 'Content-Length': contentLength },
|
||||
getHeader: () => contentLength,
|
||||
},
|
||||
}));
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
await request({
|
||||
axios,
|
||||
url: '/test',
|
||||
logger,
|
||||
data: { test: 12345 },
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(connectorUsageCollector.getRequestBodyByte()).toBe(contentLength);
|
||||
});
|
||||
|
||||
test('adds request body bytes from request header on a failed', async () => {
|
||||
const contentLength = 12;
|
||||
axiosMock.mockImplementation(
|
||||
() =>
|
||||
new AxiosError('failed', '500', undefined, {
|
||||
headers: { 'Content-Length': contentLength },
|
||||
})
|
||||
);
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
|
||||
try {
|
||||
await request({
|
||||
axios,
|
||||
url: '/test',
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
} catch (e) {
|
||||
expect(connectorUsageCollector.getRequestBodyByte()).toBe(contentLength);
|
||||
}
|
||||
});
|
||||
|
||||
test('adds request body bytes from data when request header does not exist', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
const data = { test: 12345 };
|
||||
|
||||
await request({
|
||||
axios,
|
||||
url: '/test',
|
||||
logger,
|
||||
data,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(connectorUsageCollector.getRequestBodyByte()).toBe(
|
||||
Buffer.byteLength(JSON.stringify(data), 'utf8')
|
||||
);
|
||||
});
|
||||
|
||||
test('it have been called with proper proxy agent for a valid url', async () => {
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxySSLSettings: {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { Logger } from '@kbn/core/server';
|
||||
import { getCustomAgents } from './get_custom_agents';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { SSLSettings } from '../types';
|
||||
import { ConnectorUsageCollector, SSLSettings } from '../types';
|
||||
import { combineHeadersWithBasicAuthHeader } from './get_basic_auth_header';
|
||||
|
||||
export const request = async <T = unknown>({
|
||||
|
@ -30,6 +30,7 @@ export const request = async <T = unknown>({
|
|||
headers,
|
||||
sslOverrides,
|
||||
timeout,
|
||||
connectorUsageCollector,
|
||||
...config
|
||||
}: {
|
||||
axios: AxiosInstance;
|
||||
|
@ -41,6 +42,7 @@ export const request = async <T = unknown>({
|
|||
headers?: Record<string, AxiosHeaderValue>;
|
||||
timeout?: number;
|
||||
sslOverrides?: SSLSettings;
|
||||
connectorUsageCollector?: ConnectorUsageCollector;
|
||||
} & AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
if (!isEmpty(axios?.defaults?.baseURL ?? '')) {
|
||||
throw new Error(
|
||||
|
@ -64,18 +66,31 @@ export const request = async <T = unknown>({
|
|||
headers,
|
||||
});
|
||||
|
||||
return await axios(url, {
|
||||
...restConfig,
|
||||
method,
|
||||
headers: headersWithBasicAuth,
|
||||
...(data ? { data } : {}),
|
||||
// use httpAgent and httpsAgent and set axios proxy: false, to be able to handle fail on invalid certs
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
proxy: false,
|
||||
maxContentLength,
|
||||
timeout: Math.max(settingsTimeout, timeout ?? 0),
|
||||
});
|
||||
try {
|
||||
const result = await axios(url, {
|
||||
...restConfig,
|
||||
method,
|
||||
headers: headersWithBasicAuth,
|
||||
...(data ? { data } : {}),
|
||||
// use httpAgent and httpsAgent and set axios proxy: false, to be able to handle fail on invalid certs
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
proxy: false,
|
||||
maxContentLength,
|
||||
timeout: Math.max(settingsTimeout, timeout ?? 0),
|
||||
});
|
||||
|
||||
if (connectorUsageCollector) {
|
||||
connectorUsageCollector.addRequestBodyBytes(result, data);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (connectorUsageCollector) {
|
||||
connectorUsageCollector.addRequestBodyBytes(error, data);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const patch = async <T = unknown>({
|
||||
|
@ -84,12 +99,14 @@ export const patch = async <T = unknown>({
|
|||
data,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
}: {
|
||||
axios: AxiosInstance;
|
||||
url: string;
|
||||
data: T;
|
||||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
connectorUsageCollector?: ConnectorUsageCollector;
|
||||
}): Promise<AxiosResponse> => {
|
||||
return request({
|
||||
axios,
|
||||
|
@ -98,6 +115,7 @@ export const patch = async <T = unknown>({
|
|||
method: 'patch',
|
||||
data,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
spaceId: 'default',
|
||||
name: 'test name',
|
||||
actionExecutionId: '123abc',
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
'@timestamp': '1970-01-01T00:00:00.000Z',
|
||||
|
@ -64,6 +65,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
},
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
|
@ -92,6 +94,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
},
|
||||
],
|
||||
actionExecutionId: '123abc',
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -118,6 +121,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
|
@ -145,6 +149,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
},
|
||||
],
|
||||
actionExecutionId: '123abc',
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -163,6 +168,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
|
@ -192,6 +198,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
name: 'test name',
|
||||
actionExecutionId: '123abc',
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -220,6 +227,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
},
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
|
@ -255,6 +263,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
},
|
||||
],
|
||||
actionExecutionId: '123abc',
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -289,6 +298,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
|
@ -319,6 +329,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
actionExecutionId: '123abc',
|
||||
source: asHttpRequestExecutionSource(httpServerMock.createKibanaRequest()),
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -345,6 +356,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
source: 'http_request',
|
||||
|
@ -376,6 +388,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
actionExecutionId: '123abc',
|
||||
source: asHttpRequestExecutionSource(httpServerMock.createKibanaRequest()),
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -402,6 +415,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
source: 'http_request',
|
||||
|
@ -433,6 +447,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
actionExecutionId: '123abc',
|
||||
isInMemory: true,
|
||||
actionTypeId: '.slack',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -460,6 +475,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
type_id: '.slack',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
|
|
|
@ -37,6 +37,7 @@ interface CreateActionEventLogRecordParams {
|
|||
relatedSavedObjects?: RelatedSavedObjects;
|
||||
isInMemory?: boolean;
|
||||
source?: ActionExecutionSource<unknown>;
|
||||
actionTypeId: string;
|
||||
}
|
||||
|
||||
export function createActionEventLogRecordObject(params: CreateActionEventLogRecordParams): Event {
|
||||
|
@ -54,6 +55,7 @@ export function createActionEventLogRecordObject(params: CreateActionEventLogRec
|
|||
isInMemory,
|
||||
actionId,
|
||||
source,
|
||||
actionTypeId,
|
||||
} = params;
|
||||
|
||||
const kibanaAlertRule = {
|
||||
|
@ -89,6 +91,7 @@ export function createActionEventLogRecordObject(params: CreateActionEventLogRec
|
|||
action: {
|
||||
...(name ? { name } : {}),
|
||||
id: actionId,
|
||||
type_id: actionTypeId,
|
||||
execution: {
|
||||
uuid: actionExecutionId,
|
||||
},
|
||||
|
|
|
@ -12,12 +12,14 @@ import { actionsConfigMock } from '../actions_config.mock';
|
|||
import { actionsMock } from '../mocks';
|
||||
import { TestCaseConnector } from './mocks';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { ConnectorUsageCollector } from '../usage';
|
||||
|
||||
describe('CaseConnector', () => {
|
||||
let logger: MockedLogger;
|
||||
let services: ReturnType<typeof actionsMock.createServices>;
|
||||
let mockedActionsConfig: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let service: TestCaseConnector;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
const pushToServiceIncidentParamsSchema = {
|
||||
name: schema.string(),
|
||||
category: schema.nullable(schema.string()),
|
||||
|
@ -57,6 +59,11 @@ describe('CaseConnector', () => {
|
|||
},
|
||||
pushToServiceIncidentParamsSchema
|
||||
);
|
||||
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sub actions', () => {
|
||||
|
@ -191,7 +198,7 @@ describe('CaseConnector', () => {
|
|||
|
||||
describe('pushToService', () => {
|
||||
it('should create an incident if externalId is null', async () => {
|
||||
const res = await service.pushToService(pushToServiceParams);
|
||||
const res = await service.pushToService(pushToServiceParams, connectorUsageCollector);
|
||||
expect(res).toEqual({
|
||||
id: 'create-incident',
|
||||
title: 'Test incident',
|
||||
|
@ -201,10 +208,13 @@ describe('CaseConnector', () => {
|
|||
});
|
||||
|
||||
it('should update an incident if externalId is not null', async () => {
|
||||
const res = await service.pushToService({
|
||||
incident: { ...pushToServiceParams.incident, externalId: 'test-id' },
|
||||
comments: [],
|
||||
});
|
||||
const res = await service.pushToService(
|
||||
{
|
||||
incident: { ...pushToServiceParams.incident, externalId: 'test-id' },
|
||||
comments: [],
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(res).toEqual({
|
||||
id: 'update-incident',
|
||||
|
@ -215,13 +225,16 @@ describe('CaseConnector', () => {
|
|||
});
|
||||
|
||||
it('should add comments', async () => {
|
||||
const res = await service.pushToService({
|
||||
...pushToServiceParams,
|
||||
comments: [
|
||||
{ comment: 'comment-1', commentId: 'comment-id-1' },
|
||||
{ comment: 'comment-2', commentId: 'comment-id-2' },
|
||||
],
|
||||
});
|
||||
const res = await service.pushToService(
|
||||
{
|
||||
...pushToServiceParams,
|
||||
comments: [
|
||||
{ comment: 'comment-1', commentId: 'comment-id-1' },
|
||||
{ comment: 'comment-2', commentId: 'comment-id-2' },
|
||||
],
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(res).toEqual({
|
||||
id: 'create-incident',
|
||||
|
@ -242,11 +255,14 @@ describe('CaseConnector', () => {
|
|||
});
|
||||
|
||||
it.each([[undefined], [null]])('should throw if externalId is %p', async (comments) => {
|
||||
const res = await service.pushToService({
|
||||
...pushToServiceParams,
|
||||
// @ts-expect-error
|
||||
comments,
|
||||
});
|
||||
const res = await service.pushToService(
|
||||
{
|
||||
...pushToServiceParams,
|
||||
// @ts-expect-error
|
||||
comments,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(res).toEqual({
|
||||
id: 'create-incident',
|
||||
|
@ -257,10 +273,13 @@ describe('CaseConnector', () => {
|
|||
});
|
||||
|
||||
it('should not add comments if comments are an empty array', async () => {
|
||||
const res = await service.pushToService({
|
||||
...pushToServiceParams,
|
||||
comments: [],
|
||||
});
|
||||
const res = await service.pushToService(
|
||||
{
|
||||
...pushToServiceParams,
|
||||
comments: [],
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(res).toEqual({
|
||||
id: 'create-incident',
|
||||
|
|
|
@ -9,22 +9,38 @@ import { schema, Type } from '@kbn/config-schema';
|
|||
import { ExternalServiceIncidentResponse, PushToServiceResponse } from './types';
|
||||
import { SubActionConnector } from './sub_action_connector';
|
||||
import { ServiceParams } from './types';
|
||||
import { ConnectorUsageCollector } from '../usage';
|
||||
|
||||
export interface CaseConnectorInterface<Incident, GetIncidentResponse> {
|
||||
addComment: ({ incidentId, comment }: { incidentId: string; comment: string }) => Promise<void>;
|
||||
createIncident: (incident: Incident) => Promise<ExternalServiceIncidentResponse>;
|
||||
updateIncident: ({
|
||||
incidentId,
|
||||
incident,
|
||||
}: {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
}) => Promise<ExternalServiceIncidentResponse>;
|
||||
getIncident: ({ id }: { id: string }) => Promise<GetIncidentResponse>;
|
||||
pushToService: (params: {
|
||||
incident: { externalId: string | null } & Incident;
|
||||
comments: Array<{ commentId: string; comment: string }>;
|
||||
}) => Promise<PushToServiceResponse>;
|
||||
addComment: (
|
||||
{ incidentId, comment }: { incidentId: string; comment: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) => Promise<void>;
|
||||
createIncident: (
|
||||
incident: Incident,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) => Promise<ExternalServiceIncidentResponse>;
|
||||
updateIncident: (
|
||||
{
|
||||
incidentId,
|
||||
incident,
|
||||
}: {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) => Promise<ExternalServiceIncidentResponse>;
|
||||
getIncident: (
|
||||
{ id }: { id: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) => Promise<GetIncidentResponse>;
|
||||
pushToService: (
|
||||
params: {
|
||||
incident: { externalId: string | null } & Incident;
|
||||
comments: Array<{ commentId: string; comment: string }>;
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) => Promise<PushToServiceResponse>;
|
||||
}
|
||||
|
||||
export abstract class CaseConnector<Config, Secrets, Incident, GetIncidentResponse>
|
||||
|
@ -56,50 +72,71 @@ export abstract class CaseConnector<Config, Secrets, Incident, GetIncidentRespon
|
|||
});
|
||||
}
|
||||
|
||||
public abstract addComment({
|
||||
incidentId,
|
||||
comment,
|
||||
}: {
|
||||
incidentId: string;
|
||||
comment: string;
|
||||
}): Promise<void>;
|
||||
public abstract addComment(
|
||||
{
|
||||
incidentId,
|
||||
comment,
|
||||
}: {
|
||||
incidentId: string;
|
||||
comment: string;
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<void>;
|
||||
|
||||
public abstract createIncident(incident: Incident): Promise<ExternalServiceIncidentResponse>;
|
||||
public abstract updateIncident({
|
||||
incidentId,
|
||||
incident,
|
||||
}: {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
}): Promise<ExternalServiceIncidentResponse>;
|
||||
public abstract getIncident({ id }: { id: string }): Promise<GetIncidentResponse>;
|
||||
public abstract createIncident(
|
||||
incident: Incident,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<ExternalServiceIncidentResponse>;
|
||||
public abstract updateIncident(
|
||||
{
|
||||
incidentId,
|
||||
incident,
|
||||
}: {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<ExternalServiceIncidentResponse>;
|
||||
public abstract getIncident(
|
||||
{ id }: { id: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<GetIncidentResponse>;
|
||||
|
||||
public async pushToService(params: {
|
||||
incident: { externalId: string | null } & Incident;
|
||||
comments: Array<{ commentId: string; comment: string }>;
|
||||
}) {
|
||||
public async pushToService(
|
||||
params: {
|
||||
incident: { externalId: string | null } & Incident;
|
||||
comments: Array<{ commentId: string; comment: string }>;
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const { incident, comments } = params;
|
||||
const { externalId, ...rest } = incident;
|
||||
|
||||
let res: PushToServiceResponse;
|
||||
|
||||
if (externalId != null) {
|
||||
res = await this.updateIncident({
|
||||
incidentId: externalId,
|
||||
incident: rest as Incident,
|
||||
});
|
||||
res = await this.updateIncident(
|
||||
{
|
||||
incidentId: externalId,
|
||||
incident: rest as Incident,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
} else {
|
||||
res = await this.createIncident(rest as Incident);
|
||||
res = await this.createIncident(rest as Incident, connectorUsageCollector);
|
||||
}
|
||||
|
||||
if (comments && Array.isArray(comments) && comments.length > 0) {
|
||||
res.comments = [];
|
||||
|
||||
for (const currentComment of comments) {
|
||||
await this.addComment({
|
||||
incidentId: res.id,
|
||||
comment: currentComment.comment,
|
||||
});
|
||||
await this.addComment(
|
||||
{
|
||||
incidentId: res.id,
|
||||
comment: currentComment.comment,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
res.comments = [
|
||||
...(res.comments ?? []),
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from './mocks';
|
||||
import { IService, ServiceParams } from './types';
|
||||
import { getErrorSource, TaskErrorSource } from '@kbn/task-manager-plugin/server/task_running';
|
||||
import { ConnectorUsageCollector } from '../usage';
|
||||
|
||||
describe('Executor', () => {
|
||||
const actionId = 'test-action-id';
|
||||
|
@ -30,6 +31,7 @@ describe('Executor', () => {
|
|||
let logger: MockedLogger;
|
||||
let services: ReturnType<typeof actionsMock.createServices>;
|
||||
let mockedActionsConfig: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
const createExecutor = (Service: IService<TestConfig, TestSecrets>) => {
|
||||
const connector = {
|
||||
|
@ -55,6 +57,10 @@ describe('Executor', () => {
|
|||
logger = loggingSystemMock.createLogger();
|
||||
services = actionsMock.createServices();
|
||||
mockedActionsConfig = actionsConfigMock.create();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute correctly', async () => {
|
||||
|
@ -68,6 +74,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -90,6 +97,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -112,6 +120,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -132,6 +141,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -153,6 +163,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).rejects.toThrowError('You should register at least one subAction for your connector type');
|
||||
});
|
||||
|
@ -169,6 +180,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
'Sub action "not-exist" is not registered. Connector id: test-action-id. Connector name: Test. Connector type: .test'
|
||||
|
@ -187,6 +199,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
} catch (e) {
|
||||
expect(getErrorSource(e)).toBe(TaskErrorSource.USER);
|
||||
|
@ -208,6 +221,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
'Method "not-exist" does not exists in service. Sub action: "testUrl". Connector id: test-action-id. Connector name: Test. Connector type: .test'
|
||||
|
@ -226,6 +240,7 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
'Method "notAFunction" must be a function. Connector id: test-action-id. Connector name: Test. Connector type: .test'
|
||||
|
@ -244,9 +259,50 @@ describe('Executor', () => {
|
|||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
'Request validation failed (Error: [id]: expected value of type [string] but got [undefined])'
|
||||
);
|
||||
});
|
||||
|
||||
it('Passes connectorUsageCollector to the subAction method as a second param', async () => {
|
||||
let echoSpy;
|
||||
|
||||
const subActionParams = { id: 'test-id' };
|
||||
const connector = {
|
||||
id: '.test',
|
||||
name: 'Test',
|
||||
minimumLicenseRequired: 'basic' as const,
|
||||
supportedFeatureIds: ['alerting'],
|
||||
schema: {
|
||||
config: TestConfigSchema,
|
||||
secrets: TestSecretsSchema,
|
||||
},
|
||||
getService: (serviceParams: ServiceParams<TestConfig, TestSecrets>) => {
|
||||
const service = new TestExecutor(serviceParams);
|
||||
echoSpy = jest.spyOn(service, 'echo').mockResolvedValue(subActionParams);
|
||||
return service;
|
||||
},
|
||||
};
|
||||
|
||||
const executor = buildExecutor({
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connector,
|
||||
});
|
||||
|
||||
await executor({
|
||||
actionId,
|
||||
params: { subAction: 'echo', subActionParams },
|
||||
config,
|
||||
secrets,
|
||||
services,
|
||||
configurationUtilities: mockedActionsConfig,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(echoSpy).toHaveBeenCalledWith(subActionParams, connectorUsageCollector);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,7 +30,15 @@ export const buildExecutor = <
|
|||
logger: Logger;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
}): ExecutorType<Config, Secrets, ExecutorParams, unknown> => {
|
||||
return async ({ actionId, params, config, secrets, services, request }) => {
|
||||
return async ({
|
||||
actionId,
|
||||
params,
|
||||
config,
|
||||
secrets,
|
||||
services,
|
||||
request,
|
||||
connectorUsageCollector,
|
||||
}) => {
|
||||
const subAction = params.subAction;
|
||||
const subActionParams = params.subActionParams;
|
||||
|
||||
|
@ -88,7 +96,7 @@ export const buildExecutor = <
|
|||
}
|
||||
}
|
||||
|
||||
const data = await func.call(service, subActionParams);
|
||||
const data = await func.call(service, subActionParams, connectorUsageCollector);
|
||||
return { status: 'ok', data: data ?? {}, actionId };
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { schema, Type, TypeOf } from '@kbn/config-schema';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ConnectorUsageCollector } from '../usage';
|
||||
import { SubActionConnector } from './sub_action_connector';
|
||||
import { CaseConnector } from './case';
|
||||
import { ExternalServiceIncidentResponse, ServiceParams } from './types';
|
||||
|
@ -57,36 +58,54 @@ export class TestSubActionConnector extends SubActionConnector<TestConfig, TestS
|
|||
return `Message: ${error.response?.data.errorMessage}. Code: ${error.response?.data.errorCode}`;
|
||||
}
|
||||
|
||||
public async testUrl({ url, data = {} }: { url: string; data?: Record<string, unknown> | null }) {
|
||||
const res = await this.request({
|
||||
url,
|
||||
data,
|
||||
headers: { 'X-Test-Header': 'test' },
|
||||
responseSchema: schema.object({ status: schema.string() }),
|
||||
});
|
||||
public async testUrl(
|
||||
{ url, data = {} }: { url: string; data?: Record<string, unknown> | null },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const res = await this.request(
|
||||
{
|
||||
url,
|
||||
data,
|
||||
headers: { 'X-Test-Header': 'test' },
|
||||
responseSchema: schema.object({ status: schema.string() }),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public async testData({ data }: { data: Record<string, unknown> }) {
|
||||
const res = await this.request({
|
||||
url: 'https://example.com',
|
||||
data: this.removeNullOrUndefinedFields(data),
|
||||
headers: { 'X-Test-Header': 'test' },
|
||||
responseSchema: schema.object({ status: schema.string() }),
|
||||
});
|
||||
public async testData(
|
||||
{ data }: { data: Record<string, unknown> },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const res = await this.request(
|
||||
{
|
||||
url: 'https://example.com',
|
||||
data: this.removeNullOrUndefinedFields(data),
|
||||
headers: { 'X-Test-Header': 'test' },
|
||||
responseSchema: schema.object({ status: schema.string() }),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public async testAuth({ headers }: { headers?: Record<string, unknown> } = {}) {
|
||||
const res = await this.request({
|
||||
url: 'https://example.com',
|
||||
data: {},
|
||||
auth: { username: 'username', password: 'password' },
|
||||
headers: { 'X-Test-Header': 'test', ...headers },
|
||||
responseSchema: schema.object({ status: schema.string() }),
|
||||
});
|
||||
public async testAuth(
|
||||
{ headers }: { headers?: Record<string, unknown> } = {},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const res = await this.request(
|
||||
{
|
||||
url: 'https://example.com',
|
||||
data: {},
|
||||
auth: { username: 'username', password: 'password' },
|
||||
headers: { 'X-Test-Header': 'test', ...headers },
|
||||
responseSchema: schema.object({ status: schema.string() }),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { actionsMock } from '../mocks';
|
|||
import { TestSubActionConnector } from './mocks';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import * as utils from '../lib/axios_utils';
|
||||
import { ConnectorUsageCollector } from '../usage';
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
|
@ -43,6 +44,7 @@ describe('SubActionConnector', () => {
|
|||
let services: ReturnType<typeof actionsMock.createServices>;
|
||||
let mockedActionsConfig: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let service: TestSubActionConnector;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -70,6 +72,11 @@ describe('SubActionConnector', () => {
|
|||
secrets: { username: 'elastic', password: 'changeme' },
|
||||
services,
|
||||
});
|
||||
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sub actions', () => {
|
||||
|
@ -85,34 +92,37 @@ describe('SubActionConnector', () => {
|
|||
|
||||
describe('URL validation', () => {
|
||||
it('removes double slashes correctly', async () => {
|
||||
await service.testUrl({ url: 'https://example.com//api///test-endpoint' });
|
||||
await service.testUrl(
|
||||
{ url: 'https://example.com//api///test-endpoint' },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(requestMock.mock.calls[0][0].url).toBe('https://example.com/api/test-endpoint');
|
||||
});
|
||||
|
||||
it('removes the ending slash correctly', async () => {
|
||||
await service.testUrl({ url: 'https://example.com/' });
|
||||
await service.testUrl({ url: 'https://example.com/' }, connectorUsageCollector);
|
||||
expect(requestMock.mock.calls[0][0].url).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('throws an error if the url is invalid', async () => {
|
||||
expect.assertions(1);
|
||||
await expect(async () => service.testUrl({ url: 'invalid-url' })).rejects.toThrow(
|
||||
'URL Error: Invalid URL: invalid-url'
|
||||
);
|
||||
await expect(async () =>
|
||||
service.testUrl({ url: 'invalid-url' }, connectorUsageCollector)
|
||||
).rejects.toThrow('URL Error: Invalid URL: invalid-url');
|
||||
});
|
||||
|
||||
it('throws an error if the url starts with backslashes', async () => {
|
||||
expect.assertions(1);
|
||||
await expect(async () => service.testUrl({ url: '//example.com/foo' })).rejects.toThrow(
|
||||
'URL Error: Invalid URL: //example.com/foo'
|
||||
);
|
||||
await expect(async () =>
|
||||
service.testUrl({ url: '//example.com/foo' }, connectorUsageCollector)
|
||||
).rejects.toThrow('URL Error: Invalid URL: //example.com/foo');
|
||||
});
|
||||
|
||||
it('throws an error if the protocol is not supported', async () => {
|
||||
expect.assertions(1);
|
||||
await expect(async () => service.testUrl({ url: 'ftp://example.com' })).rejects.toThrow(
|
||||
'URL Error: Invalid protocol'
|
||||
);
|
||||
await expect(async () =>
|
||||
service.testUrl({ url: 'ftp://example.com' }, connectorUsageCollector)
|
||||
).rejects.toThrow('URL Error: Invalid protocol');
|
||||
});
|
||||
|
||||
it('throws if the host is the URI is not allowed', async () => {
|
||||
|
@ -122,15 +132,15 @@ describe('SubActionConnector', () => {
|
|||
throw new Error('URI is not allowed');
|
||||
});
|
||||
|
||||
await expect(async () => service.testUrl({ url: 'https://example.com' })).rejects.toThrow(
|
||||
'error configuring connector action: URI is not allowed'
|
||||
);
|
||||
await expect(async () =>
|
||||
service.testUrl({ url: 'https://example.com' }, connectorUsageCollector)
|
||||
).rejects.toThrow('error configuring connector action: URI is not allowed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data', () => {
|
||||
it('sets data to an empty object if the data are null', async () => {
|
||||
await service.testUrl({ url: 'https://example.com', data: null });
|
||||
await service.testUrl({ url: 'https://example.com', data: null }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
const { data } = requestMock.mock.calls[0][0];
|
||||
|
@ -138,7 +148,10 @@ describe('SubActionConnector', () => {
|
|||
});
|
||||
|
||||
it('pass data to axios correctly if not null', async () => {
|
||||
await service.testUrl({ url: 'https://example.com', data: { foo: 'foo' } });
|
||||
await service.testUrl(
|
||||
{ url: 'https://example.com', data: { foo: 'foo' } },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
const { data } = requestMock.mock.calls[0][0];
|
||||
|
@ -146,7 +159,10 @@ describe('SubActionConnector', () => {
|
|||
});
|
||||
|
||||
it('removeNullOrUndefinedFields: removes null values and undefined values correctly', async () => {
|
||||
await service.testData({ data: { foo: 'foo', bar: null, baz: undefined } });
|
||||
await service.testData(
|
||||
{ data: { foo: 'foo', bar: null, baz: undefined } },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
const { data } = requestMock.mock.calls[0][0];
|
||||
|
@ -167,7 +183,7 @@ describe('SubActionConnector', () => {
|
|||
|
||||
describe('Fetching', () => {
|
||||
it('fetch correctly', async () => {
|
||||
const res = await service.testUrl({ url: 'https://example.com' });
|
||||
const res = await service.testUrl({ url: 'https://example.com' }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(requestMock).toBeCalledWith({
|
||||
|
@ -181,6 +197,7 @@ describe('SubActionConnector', () => {
|
|||
'X-Test-Header': 'test',
|
||||
},
|
||||
url: 'https://example.com',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(logger.debug).toBeCalledWith(
|
||||
|
@ -192,7 +209,9 @@ describe('SubActionConnector', () => {
|
|||
|
||||
it('validates the response correctly', async () => {
|
||||
requestMock.mockReturnValue({ data: { invalidField: 'test' } });
|
||||
await expect(async () => service.testUrl({ url: 'https://example.com' })).rejects.toThrow(
|
||||
await expect(async () =>
|
||||
service.testUrl({ url: 'https://example.com' }, connectorUsageCollector)
|
||||
).rejects.toThrow(
|
||||
'Response validation failed (Error: [status]: expected value of type [string] but got [undefined])'
|
||||
);
|
||||
});
|
||||
|
@ -202,9 +221,9 @@ describe('SubActionConnector', () => {
|
|||
throw createAxiosError();
|
||||
});
|
||||
|
||||
await expect(async () => service.testUrl({ url: 'https://example.com' })).rejects.toThrow(
|
||||
'Message: An error occurred. Code: 500'
|
||||
);
|
||||
await expect(async () =>
|
||||
service.testUrl({ url: 'https://example.com' }, connectorUsageCollector)
|
||||
).rejects.toThrow('Message: An error occurred. Code: 500');
|
||||
|
||||
expect(logger.debug).toHaveBeenLastCalledWith(
|
||||
'Request to external service failed. Connector Id: test-id. Connector type: .test. Method: get. URL: https://example.com'
|
||||
|
@ -212,7 +231,7 @@ describe('SubActionConnector', () => {
|
|||
});
|
||||
|
||||
it('converts auth axios property to a basic auth header if provided', async () => {
|
||||
await service.testAuth();
|
||||
await service.testAuth(undefined, connectorUsageCollector);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(requestMock).toBeCalledWith({
|
||||
|
@ -227,11 +246,15 @@ describe('SubActionConnector', () => {
|
|||
Authorization: `Basic ${Buffer.from('username:password').toString('base64')}`,
|
||||
},
|
||||
url: 'https://example.com',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not override an authorization header if provided', async () => {
|
||||
await service.testAuth({ headers: { Authorization: 'Bearer my_token' } });
|
||||
await service.testAuth(
|
||||
{ headers: { Authorization: 'Bearer my_token' } },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledTimes(1);
|
||||
expect(requestMock).toBeCalledWith({
|
||||
|
@ -246,6 +269,7 @@ describe('SubActionConnector', () => {
|
|||
Authorization: 'Bearer my_token',
|
||||
},
|
||||
url: 'https://example.com',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ import { IncomingMessage } from 'http';
|
|||
import { PassThrough } from 'stream';
|
||||
import { KibanaRequest } from '@kbn/core-http-server';
|
||||
import { inspect } from 'util';
|
||||
import { ConnectorUsageCollector } from '../usage';
|
||||
import { assertURL } from './helpers/validators';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { SubAction, SubActionRequestParams } from './types';
|
||||
|
@ -130,15 +131,18 @@ export abstract class SubActionConnector<Config, Secrets> {
|
|||
|
||||
protected abstract getResponseErrorMessage(error: AxiosError): string;
|
||||
|
||||
protected async request<R>({
|
||||
url,
|
||||
data,
|
||||
method = 'get',
|
||||
responseSchema,
|
||||
headers,
|
||||
timeout,
|
||||
...config
|
||||
}: SubActionRequestParams<R>): Promise<AxiosResponse<R>> {
|
||||
protected async request<R>(
|
||||
{
|
||||
url,
|
||||
data,
|
||||
method = 'get',
|
||||
responseSchema,
|
||||
headers,
|
||||
timeout,
|
||||
...config
|
||||
}: SubActionRequestParams<R>,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<AxiosResponse<R>> {
|
||||
try {
|
||||
this.assertURL(url);
|
||||
this.ensureUriAllowed(url);
|
||||
|
@ -160,6 +164,7 @@ export abstract class SubActionConnector<Config, Secrets> {
|
|||
configurationUtilities: this.configurationUtilities,
|
||||
headers: this.getHeaders(auth, headers as AxiosHeaders),
|
||||
timeout,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
this.validateResponse(responseSchema, res.data);
|
||||
|
|
|
@ -39,11 +39,11 @@ export type ActionTypeSecrets = Record<string, unknown>;
|
|||
export type ActionTypeParams = Record<string, unknown>;
|
||||
export type ConnectorTokenClientContract = PublicMethodsOf<ConnectorTokenClient>;
|
||||
|
||||
import type { ActionExecutionSource } from './lib';
|
||||
import { Connector, ConnectorWithExtraFindData } from './application/connector/types';
|
||||
export type { ActionExecutionSource } from './lib';
|
||||
|
||||
import type { ActionExecutionSource } from './lib';
|
||||
export { ActionExecutionSourceType } from './lib';
|
||||
import { ConnectorUsageCollector } from './usage';
|
||||
export { ConnectorUsageCollector } from './usage';
|
||||
|
||||
export interface Services {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
|
@ -88,6 +88,7 @@ export interface ActionTypeExecutorOptions<
|
|||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
source?: ActionExecutionSource<unknown>;
|
||||
request?: KibanaRequest;
|
||||
connectorUsageCollector: ConnectorUsageCollector;
|
||||
}
|
||||
|
||||
export type ActionResult = Connector;
|
||||
|
|
|
@ -12,11 +12,8 @@ import {
|
|||
ExecuteOptions,
|
||||
ExecutionResponse,
|
||||
} from '../create_unsecured_execute_function';
|
||||
import {
|
||||
ActionExecutorContract,
|
||||
asNotificationExecutionSource,
|
||||
type RelatedSavedObjects,
|
||||
} from '../lib';
|
||||
import { ActionExecutorContract, asNotificationExecutionSource } from '../lib';
|
||||
import type { RelatedSavedObjects } from '../lib';
|
||||
import { ActionTypeExecutorResult, InMemoryConnector } from '../types';
|
||||
import { asBackgroundTaskExecutionSource } from '../lib/action_execution_source';
|
||||
import { ConnectorWithExtraFindData } from '../application/connector/types';
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConnectorUsageCollector } from '../types';
|
||||
import { AxiosHeaders, AxiosResponse } from 'axios';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
||||
describe('ConnectorUsageCollector', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
test('it collects requestBodyBytes from response.request.headers', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
const data = { test: 'foo' };
|
||||
const contentLength = Buffer.byteLength(JSON.stringify(data), 'utf8');
|
||||
|
||||
const axiosResponse: AxiosResponse = {
|
||||
data,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: { headers: new AxiosHeaders() },
|
||||
request: {
|
||||
headers: { 'Content-Length': contentLength },
|
||||
getHeader: () => contentLength,
|
||||
},
|
||||
};
|
||||
|
||||
connectorUsageCollector.addRequestBodyBytes(axiosResponse, data);
|
||||
|
||||
expect(connectorUsageCollector.getRequestBodyByte()).toBe(contentLength);
|
||||
|
||||
connectorUsageCollector.addRequestBodyBytes(axiosResponse, data);
|
||||
|
||||
expect(connectorUsageCollector.getRequestBodyByte()).toBe(contentLength + contentLength);
|
||||
});
|
||||
test('it collects requestBodyBytes from data when header is is missing', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
const data = { test: 'foo' };
|
||||
const contentLength = Buffer.byteLength(JSON.stringify(data), 'utf8');
|
||||
|
||||
const axiosResponse: AxiosResponse = {
|
||||
data,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: { headers: new AxiosHeaders() },
|
||||
request: {
|
||||
getHeader: () => undefined,
|
||||
},
|
||||
};
|
||||
|
||||
connectorUsageCollector.addRequestBodyBytes(axiosResponse, data);
|
||||
|
||||
expect(connectorUsageCollector.getRequestBodyByte()).toBe(contentLength);
|
||||
|
||||
connectorUsageCollector.addRequestBodyBytes(axiosResponse, data);
|
||||
|
||||
expect(connectorUsageCollector.getRequestBodyByte()).toBe(contentLength + contentLength);
|
||||
});
|
||||
|
||||
test('it logs an error when the body cannot be stringified ', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
|
||||
const data = {
|
||||
name: 'arun',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
data.foo = data; // this is to force JSON.stringify to throw
|
||||
|
||||
const axiosResponse: AxiosResponse = {
|
||||
data,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: { headers: new AxiosHeaders() },
|
||||
request: {
|
||||
getHeader: () => undefined,
|
||||
},
|
||||
};
|
||||
|
||||
connectorUsageCollector.addRequestBodyBytes(axiosResponse, data);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Request body bytes couldn't be calculated, Error: ")
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { isUndefined } from 'lodash';
|
||||
|
||||
interface ConnectorUsage {
|
||||
requestBodyBytes: number;
|
||||
}
|
||||
|
||||
export class ConnectorUsageCollector {
|
||||
private connectorId: string;
|
||||
private usage: ConnectorUsage = {
|
||||
requestBodyBytes: 0,
|
||||
};
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor({ logger, connectorId }: { logger: Logger; connectorId: string }) {
|
||||
this.logger = logger;
|
||||
this.connectorId = connectorId;
|
||||
}
|
||||
|
||||
public addRequestBodyBytes(result?: AxiosError | AxiosResponse, body: string | object = '') {
|
||||
const contentLength = result?.request?.getHeader('content-length');
|
||||
let bytes = 0;
|
||||
|
||||
if (!isUndefined(contentLength)) {
|
||||
bytes = parseInt(contentLength, 10);
|
||||
} else {
|
||||
try {
|
||||
const sBody = typeof body === 'string' ? body : JSON.stringify(body);
|
||||
bytes = Buffer.byteLength(sBody, 'utf8');
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Request body bytes couldn't be calculated, Error: ${e.message}, connectorId:${this.connectorId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.usage.requestBodyBytes = this.usage.requestBodyBytes + bytes;
|
||||
}
|
||||
|
||||
public getRequestBodyByte() {
|
||||
return this.usage.requestBodyBytes;
|
||||
}
|
||||
}
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export { registerActionsUsageCollector } from './actions_usage_collector';
|
||||
export { ConnectorUsageCollector } from './connector_usage_collector';
|
||||
|
|
|
@ -482,6 +482,10 @@
|
|||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"type_id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"execution": {
|
||||
"properties": {
|
||||
"source": {
|
||||
|
@ -508,6 +512,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"properties": {
|
||||
"request_body_bytes": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,6 +212,7 @@ export const EventSchema = schema.maybe(
|
|||
schema.object({
|
||||
name: ecsString(),
|
||||
id: ecsString(),
|
||||
type_id: ecsString(),
|
||||
execution: schema.maybe(
|
||||
schema.object({
|
||||
source: ecsString(),
|
||||
|
@ -227,6 +228,11 @@ export const EventSchema = schema.maybe(
|
|||
),
|
||||
})
|
||||
),
|
||||
usage: schema.maybe(
|
||||
schema.object({
|
||||
request_body_bytes: ecsStringOrNumber(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
|
|
@ -257,6 +257,10 @@ exports.EcsCustomPropertyMappings = {
|
|||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
},
|
||||
type_id: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
},
|
||||
execution: {
|
||||
properties: {
|
||||
source: {
|
||||
|
@ -284,6 +288,13 @@ exports.EcsCustomPropertyMappings = {
|
|||
},
|
||||
},
|
||||
},
|
||||
usage: {
|
||||
properties: {
|
||||
request_body_bytes: {
|
||||
type: 'long',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
import { DEFAULT_BODY } from '../../../public/connector_types/bedrock/constants';
|
||||
import { initDashboard } from '../lib/gen_ai/create_gen_ai_dashboard';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
jest.mock('../lib/gen_ai/create_gen_ai_dashboard');
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -37,6 +38,7 @@ describe('BedrockConnector', () => {
|
|||
completion: mockResponseString,
|
||||
stop_reason: 'stop_sequence',
|
||||
};
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
const claude3Response = {
|
||||
id: 'compl_01E7D3vTBHdNdKWCe6zALmLH',
|
||||
|
@ -57,12 +59,18 @@ describe('BedrockConnector', () => {
|
|||
headers: {},
|
||||
data: claude3Response,
|
||||
};
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
||||
mockError = jest.fn().mockImplementation(() => {
|
||||
throw new Error('API Error');
|
||||
});
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
const connector = new BedrockConnector({
|
||||
|
@ -73,7 +81,7 @@ describe('BedrockConnector', () => {
|
|||
defaultModel: DEFAULT_BEDROCK_MODEL,
|
||||
},
|
||||
secrets: { accessKey: '123', secret: 'secret' },
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
|
||||
|
@ -85,7 +93,13 @@ describe('BedrockConnector', () => {
|
|||
|
||||
describe('runApi', () => {
|
||||
it('the aws signature has non-streaming headers', async () => {
|
||||
await connector.runApi({ body: DEFAULT_BODY });
|
||||
await connector.runApi(
|
||||
{ body: DEFAULT_BODY },
|
||||
new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
})
|
||||
);
|
||||
expect(mockSigner).toHaveBeenCalledWith(
|
||||
{
|
||||
body: DEFAULT_BODY,
|
||||
|
@ -101,16 +115,19 @@ describe('BedrockConnector', () => {
|
|||
);
|
||||
});
|
||||
it('the Bedrock API call is successful with Claude 3 parameters; returns the response formatted for Claude 2 along with usage object', async () => {
|
||||
const response = await connector.runApi({ body: DEFAULT_BODY });
|
||||
const response = await connector.runApi({ body: DEFAULT_BODY }, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: DEFAULT_BODY,
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: DEFAULT_BODY,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({
|
||||
...claude2Response,
|
||||
usage: claude3Response.usage,
|
||||
|
@ -128,16 +145,19 @@ describe('BedrockConnector', () => {
|
|||
});
|
||||
// @ts-ignore
|
||||
connector.request = mockRequest;
|
||||
const response = await connector.runApi({ body: v2Body });
|
||||
const response = await connector.runApi({ body: v2Body }, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: v2Body,
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: v2Body,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(claude2Response);
|
||||
});
|
||||
|
||||
|
@ -145,7 +165,15 @@ describe('BedrockConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.runApi({ body: DEFAULT_BODY })).rejects.toThrow('API Error');
|
||||
await expect(
|
||||
connector.runApi(
|
||||
{ body: DEFAULT_BODY },
|
||||
new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
})
|
||||
)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -170,7 +198,7 @@ describe('BedrockConnector', () => {
|
|||
};
|
||||
|
||||
it('the aws signature has streaming headers', async () => {
|
||||
await connector.invokeStream(aiAssistantBody);
|
||||
await connector.invokeStream(aiAssistantBody, connectorUsageCollector);
|
||||
|
||||
expect(mockSigner).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -189,170 +217,197 @@ describe('BedrockConnector', () => {
|
|||
});
|
||||
|
||||
it('the API call is successful with correct request parameters', async () => {
|
||||
await connector.invokeStream(aiAssistantBody);
|
||||
await connector.invokeStream(aiAssistantBody, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({ ...JSON.parse(DEFAULT_BODY), temperature: 0 }),
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({ ...JSON.parse(DEFAULT_BODY), temperature: 0 }),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('signal and timeout is properly passed to streamApi', async () => {
|
||||
const signal = jest.fn();
|
||||
const timeout = 180000;
|
||||
await connector.invokeStream({ ...aiAssistantBody, timeout, signal });
|
||||
await connector.invokeStream(
|
||||
{ ...aiAssistantBody, timeout, signal },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({ ...JSON.parse(DEFAULT_BODY), temperature: 0 }),
|
||||
timeout,
|
||||
signal,
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({ ...JSON.parse(DEFAULT_BODY), temperature: 0 }),
|
||||
timeout,
|
||||
signal,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('ensureMessageFormat - formats messages from user, assistant, and system', async () => {
|
||||
await connector.invokeStream({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
responseType: 'stream',
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot',
|
||||
await connector.invokeStream(
|
||||
{
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
responseType: 'stream',
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot',
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('ensureMessageFormat - formats messages from when double user/assistant occurs', async () => {
|
||||
await connector.invokeStream({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'But I can be naughty',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'I can be naughty too',
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content: 'This is extra tricky',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
responseType: 'stream',
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot\nThis is extra tricky',
|
||||
await connector.invokeStream(
|
||||
{
|
||||
messages: [
|
||||
{ content: 'Hi, I am a good chatbot\nBut I can be naughty', role: 'assistant' },
|
||||
{ content: 'What is 2+2?\nI can be naughty too', role: 'user' },
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'But I can be naughty',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'I can be naughty too',
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content: 'This is extra tricky',
|
||||
},
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
responseType: 'stream',
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot\nThis is extra tricky',
|
||||
messages: [
|
||||
{ content: 'Hi, I am a good chatbot\nBut I can be naughty', role: 'assistant' },
|
||||
{ content: 'What is 2+2?\nI can be naughty too', role: 'user' },
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('formats the system message as a user message for claude<2.1', async () => {
|
||||
const modelOverride = 'anthropic.claude-v2';
|
||||
|
||||
await connector.invokeStream({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
model: modelOverride,
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
responseType: 'stream',
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${modelOverride}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot',
|
||||
await connector.invokeStream(
|
||||
{
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
model: modelOverride,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
responseType: 'stream',
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${modelOverride}/invoke-with-response-stream`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot',
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('responds with a readable stream', async () => {
|
||||
const response = await connector.invokeStream(aiAssistantBody);
|
||||
const response = await connector.invokeStream(aiAssistantBody, connectorUsageCollector);
|
||||
expect(response instanceof PassThrough).toEqual(true);
|
||||
});
|
||||
|
||||
|
@ -360,7 +415,9 @@ describe('BedrockConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.invokeStream(aiAssistantBody)).rejects.toThrow('API Error');
|
||||
await expect(
|
||||
connector.invokeStream(aiAssistantBody, connectorUsageCollector)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -376,175 +433,201 @@ describe('BedrockConnector', () => {
|
|||
};
|
||||
|
||||
it('the API call is successful with correct parameters', async () => {
|
||||
const response = await connector.invokeAI(aiAssistantBody);
|
||||
const response = await connector.invokeAI(aiAssistantBody, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...JSON.parse(DEFAULT_BODY),
|
||||
messages: [{ content: 'Hello world', role: 'user' }],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...JSON.parse(DEFAULT_BODY),
|
||||
messages: [{ content: 'Hello world', role: 'user' }],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response.message).toEqual(mockResponseString);
|
||||
});
|
||||
|
||||
it('formats messages from user, assistant, and system', async () => {
|
||||
const response = await connector.invokeAI({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot',
|
||||
const response = await connector.invokeAI(
|
||||
{
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'Be a good chatbot',
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response.message).toEqual(mockResponseString);
|
||||
});
|
||||
|
||||
it('adds system message from argument', async () => {
|
||||
const response = await connector.invokeAI({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
system: 'This is a system message',
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'This is a system message',
|
||||
const response = await connector.invokeAI(
|
||||
{
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
system: 'This is a system message',
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'This is a system message',
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response.message).toEqual(mockResponseString);
|
||||
});
|
||||
|
||||
it('combines argument system message with conversation system message', async () => {
|
||||
const response = await connector.invokeAI({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
system: 'This is a system message',
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'This is a system message\nBe a good chatbot',
|
||||
const response = await connector.invokeAI(
|
||||
{
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
{
|
||||
role: 'system',
|
||||
content: 'Be a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Hello world',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Hi, I am a good chatbot',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'What is 2+2?',
|
||||
},
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
system: 'This is a system message',
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
timeout: DEFAULT_TIMEOUT_MS,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
anthropic_version: 'bedrock-2023-05-31',
|
||||
system: 'This is a system message\nBe a good chatbot',
|
||||
messages: [
|
||||
{ content: 'Hello world', role: 'user' },
|
||||
{ content: 'Hi, I am a good chatbot', role: 'assistant' },
|
||||
{ content: 'What is 2+2?', role: 'user' },
|
||||
],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response.message).toEqual(mockResponseString);
|
||||
});
|
||||
it('signal and timeout is properly passed to runApi', async () => {
|
||||
const signal = jest.fn();
|
||||
const timeout = 180000;
|
||||
await connector.invokeAI({ ...aiAssistantBody, timeout, signal });
|
||||
await connector.invokeAI({ ...aiAssistantBody, timeout, signal }, connectorUsageCollector);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
signed: true,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...JSON.parse(DEFAULT_BODY),
|
||||
messages: [{ content: 'Hello world', role: 'user' }],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
timeout,
|
||||
signal,
|
||||
});
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
signed: true,
|
||||
url: `${DEFAULT_BEDROCK_URL}/model/${DEFAULT_BEDROCK_MODEL}/invoke`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiLatestResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...JSON.parse(DEFAULT_BODY),
|
||||
messages: [{ content: 'Hello world', role: 'user' }],
|
||||
max_tokens: DEFAULT_TOKEN_LIMIT,
|
||||
temperature: 0,
|
||||
}),
|
||||
timeout,
|
||||
signal,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
it('errors during API calls are properly handled', async () => {
|
||||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.invokeAI(aiAssistantBody)).rejects.toThrow('API Error');
|
||||
await expect(connector.invokeAI(aiAssistantBody, connectorUsageCollector)).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('getResponseErrorMessage', () => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { AxiosError, Method } from 'axios';
|
|||
import { IncomingMessage } from 'http';
|
||||
import { PassThrough } from 'stream';
|
||||
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { initDashboard } from '../lib/gen_ai/create_gen_ai_dashboard';
|
||||
import {
|
||||
RunActionParamsSchema,
|
||||
|
@ -194,16 +195,18 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B
|
|||
}
|
||||
|
||||
private async runApiRaw(
|
||||
params: SubActionRequestParams<RunActionResponse | InvokeAIRawActionResponse>
|
||||
params: SubActionRequestParams<RunActionResponse | InvokeAIRawActionResponse>,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<RunActionResponse | InvokeAIRawActionResponse> {
|
||||
const response = await this.request(params);
|
||||
const response = await this.request(params, connectorUsageCollector);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private async runApiLatest(
|
||||
params: SubActionRequestParams<RunApiLatestResponse>
|
||||
params: SubActionRequestParams<RunApiLatestResponse>,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<RunActionResponse> {
|
||||
const response = await this.request(params);
|
||||
const response = await this.request(params, connectorUsageCollector);
|
||||
// keeping the response the same as claude 2 for our APIs
|
||||
// adding the usage object for better token tracking
|
||||
return {
|
||||
|
@ -218,13 +221,10 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B
|
|||
* @param body The stringified request body to be sent in the POST request.
|
||||
* @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used.
|
||||
*/
|
||||
public async runApi({
|
||||
body,
|
||||
model: reqModel,
|
||||
signal,
|
||||
timeout,
|
||||
raw,
|
||||
}: RunActionParams): Promise<RunActionResponse | InvokeAIRawActionResponse> {
|
||||
public async runApi(
|
||||
{ body, model: reqModel, signal, timeout, raw }: RunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<RunActionResponse | InvokeAIRawActionResponse> {
|
||||
// set model on per request basis
|
||||
const currentModel = reqModel ?? this.model;
|
||||
const path = `/model/${currentModel}/invoke`;
|
||||
|
@ -240,13 +240,22 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B
|
|||
};
|
||||
|
||||
if (raw) {
|
||||
return this.runApiRaw({ ...requestArgs, responseSchema: InvokeAIRawActionResponseSchema });
|
||||
return this.runApiRaw(
|
||||
{ ...requestArgs, responseSchema: InvokeAIRawActionResponseSchema },
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
// possible api received deprecated arguments, which will still work with the deprecated Claude 2 models
|
||||
if (usesDeprecatedArguments(body)) {
|
||||
return this.runApiRaw({ ...requestArgs, responseSchema: RunActionResponseSchema });
|
||||
return this.runApiRaw(
|
||||
{ ...requestArgs, responseSchema: RunActionResponseSchema },
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
return this.runApiLatest({ ...requestArgs, responseSchema: RunApiLatestResponseSchema });
|
||||
return this.runApiLatest(
|
||||
{ ...requestArgs, responseSchema: RunApiLatestResponseSchema },
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -257,26 +266,27 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B
|
|||
* @param body The stringified request body to be sent in the POST request.
|
||||
* @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used.
|
||||
*/
|
||||
private async streamApi({
|
||||
body,
|
||||
model: reqModel,
|
||||
signal,
|
||||
timeout,
|
||||
}: RunActionParams): Promise<StreamingResponse> {
|
||||
private async streamApi(
|
||||
{ body, model: reqModel, signal, timeout }: RunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<StreamingResponse> {
|
||||
// set model on per request basis
|
||||
const path = `/model/${reqModel ?? this.model}/invoke-with-response-stream`;
|
||||
const signed = this.signRequest(body, path, true);
|
||||
|
||||
const response = await this.request({
|
||||
...signed,
|
||||
url: `${this.url}${path}`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: body,
|
||||
responseType: 'stream',
|
||||
signal,
|
||||
timeout,
|
||||
});
|
||||
const response = await this.request(
|
||||
{
|
||||
...signed,
|
||||
url: `${this.url}${path}`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: body,
|
||||
responseType: 'stream',
|
||||
signal,
|
||||
timeout,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return response.data.pipe(new PassThrough());
|
||||
}
|
||||
|
@ -289,24 +299,30 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B
|
|||
* @param messages An array of messages to be sent to the API
|
||||
* @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used.
|
||||
*/
|
||||
public async invokeStream({
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
signal,
|
||||
timeout,
|
||||
tools,
|
||||
}: InvokeAIActionParams | InvokeAIRawActionParams): Promise<IncomingMessage> {
|
||||
const res = (await this.streamApi({
|
||||
body: JSON.stringify(
|
||||
formatBedrockBody({ messages, stopSequences, system, temperature, tools })
|
||||
),
|
||||
public async invokeStream(
|
||||
{
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
signal,
|
||||
timeout,
|
||||
})) as unknown as IncomingMessage;
|
||||
tools,
|
||||
}: InvokeAIActionParams | InvokeAIRawActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<IncomingMessage> {
|
||||
const res = (await this.streamApi(
|
||||
{
|
||||
body: JSON.stringify(
|
||||
formatBedrockBody({ messages, stopSequences, system, temperature, tools })
|
||||
),
|
||||
model,
|
||||
signal,
|
||||
timeout,
|
||||
},
|
||||
connectorUsageCollector
|
||||
)) as unknown as IncomingMessage;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -318,54 +334,66 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B
|
|||
* @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used.
|
||||
* @returns an object with the response string as a property called message
|
||||
*/
|
||||
public async invokeAI({
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
maxTokens,
|
||||
signal,
|
||||
timeout,
|
||||
}: InvokeAIActionParams): Promise<InvokeAIActionResponse> {
|
||||
const res = (await this.runApi({
|
||||
body: JSON.stringify(
|
||||
formatBedrockBody({ messages, stopSequences, system, temperature, maxTokens })
|
||||
),
|
||||
public async invokeAI(
|
||||
{
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
maxTokens,
|
||||
signal,
|
||||
timeout,
|
||||
})) as RunActionResponse;
|
||||
}: InvokeAIActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<InvokeAIActionResponse> {
|
||||
const res = (await this.runApi(
|
||||
{
|
||||
body: JSON.stringify(
|
||||
formatBedrockBody({ messages, stopSequences, system, temperature, maxTokens })
|
||||
),
|
||||
model,
|
||||
signal,
|
||||
timeout,
|
||||
},
|
||||
connectorUsageCollector
|
||||
)) as RunActionResponse;
|
||||
return { message: res.completion.trim() };
|
||||
}
|
||||
|
||||
public async invokeAIRaw({
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
maxTokens = DEFAULT_TOKEN_LIMIT,
|
||||
signal,
|
||||
timeout,
|
||||
tools,
|
||||
anthropicVersion,
|
||||
}: InvokeAIRawActionParams): Promise<InvokeAIRawActionResponse> {
|
||||
const res = await this.runApi({
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
stop_sequences: stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
tools,
|
||||
anthropic_version: anthropicVersion,
|
||||
}),
|
||||
public async invokeAIRaw(
|
||||
{
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
maxTokens = DEFAULT_TOKEN_LIMIT,
|
||||
signal,
|
||||
timeout,
|
||||
raw: true,
|
||||
});
|
||||
tools,
|
||||
anthropicVersion,
|
||||
}: InvokeAIRawActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<InvokeAIRawActionResponse> {
|
||||
const res = await this.runApi(
|
||||
{
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
stop_sequences: stopSequences,
|
||||
system,
|
||||
temperature,
|
||||
max_tokens: maxTokens,
|
||||
tools,
|
||||
anthropic_version: anthropicVersion,
|
||||
}),
|
||||
model,
|
||||
signal,
|
||||
timeout,
|
||||
raw: true,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export async function executor(
|
|||
CasesWebhookActionParamsType
|
||||
>
|
||||
): Promise<ConnectorTypeExecutorResult<CasesWebhookExecutorResultData>> {
|
||||
const { actionId, configurationUtilities, params, logger } = execOptions;
|
||||
const { actionId, configurationUtilities, params, logger, connectorUsageCollector } = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
let data: CasesWebhookExecutorResultData | undefined;
|
||||
|
||||
|
@ -81,7 +81,8 @@ export async function executor(
|
|||
secrets: execOptions.secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { Logger } from '@kbn/core/server';
|
|||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { getBasicAuthHeader } from '@kbn/actions-plugin/server/lib';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { AuthType, WebhookMethods, SSLCertType } from '../../../common/auth/constants';
|
||||
import { CRT_FILE, KEY_FILE } from '../../../common/auth/mocks';
|
||||
|
||||
|
@ -69,12 +70,17 @@ const sslConfig: CasesWebhookPublicConfigurationType = {
|
|||
hasAuth: true,
|
||||
};
|
||||
const sslSecrets = { crt: CRT_FILE, key: KEY_FILE, password: 'foobar', user: null, pfx: null };
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
describe('Cases webhook service', () => {
|
||||
let service: ExternalService;
|
||||
let sslService: ExternalService;
|
||||
|
||||
beforeAll(() => {
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
service = createExternalService(
|
||||
actionId,
|
||||
{
|
||||
|
@ -82,7 +88,8 @@ describe('Cases webhook service', () => {
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
sslService = createExternalService(
|
||||
|
@ -92,7 +99,8 @@ describe('Cases webhook service', () => {
|
|||
secrets: sslSecrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(mockTime);
|
||||
|
@ -121,7 +129,8 @@ describe('Cases webhook service', () => {
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -135,7 +144,8 @@ describe('Cases webhook service', () => {
|
|||
secrets: { ...secrets, user: '', password: '' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -149,7 +159,8 @@ describe('Cases webhook service', () => {
|
|||
secrets: { ...secrets, user: '', password: '' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).not.toThrow();
|
||||
});
|
||||
|
@ -162,7 +173,8 @@ describe('Cases webhook service', () => {
|
|||
secrets: { ...secrets, user: 'username', password: 'password' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(axios.create).toHaveBeenCalledWith({
|
||||
|
@ -182,7 +194,8 @@ describe('Cases webhook service', () => {
|
|||
secrets: { ...secrets, user: 'username', password: 'password' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(axios.create).toHaveBeenCalledWith({
|
||||
|
@ -225,6 +238,7 @@ describe('Cases webhook service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
sslOverrides: defaultSSLOverrides,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -238,6 +252,24 @@ describe('Cases webhook service', () => {
|
|||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": [Function],
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction],
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction],
|
||||
|
@ -481,6 +513,7 @@ describe('Cases webhook service', () => {
|
|||
configurationUtilities,
|
||||
sslOverrides: defaultSSLOverrides,
|
||||
data: `{"fields":{"title":"title","description":"desc","tags":["hello","world"],"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -510,6 +543,36 @@ describe('Cases webhook service', () => {
|
|||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": [Function],
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from webhook action \\"1234\\": [HTTP 200] OK",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": "{\\"fields\\":{\\"title\\":\\"title\\",\\"description\\":\\"desc\\",\\"tags\\":[\\"hello\\",\\"world\\"],\\"project\\":{\\"key\\":\\"ROC\\"},\\"issuetype\\":{\\"id\\":\\"10024\\"}}}",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
|
@ -756,6 +819,7 @@ describe('Cases webhook service', () => {
|
|||
issuetype: { id: '10024' },
|
||||
},
|
||||
}),
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -776,6 +840,24 @@ describe('Cases webhook service', () => {
|
|||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": [Function],
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction],
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": "{\\"fields\\":{\\"title\\":\\"title\\",\\"description\\":\\"desc\\",\\"tags\\":[\\"hello\\",\\"world\\"],\\"project\\":{\\"key\\":\\"ROC\\"},\\"issuetype\\":{\\"id\\":\\"10024\\"}}}",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
|
@ -984,6 +1066,7 @@ describe('Cases webhook service', () => {
|
|||
sslOverrides: defaultSSLOverrides,
|
||||
url: 'https://coolsite.net/issue/1/comment',
|
||||
data: `{"body":"comment"}`,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1004,6 +1087,24 @@ describe('Cases webhook service', () => {
|
|||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": [Function],
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction],
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": "{\\"body\\":\\"comment\\"}",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
|
@ -1176,7 +1277,8 @@ describe('Cases webhook service', () => {
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
const res = await service.createComment(commentReq);
|
||||
expect(requestMock).not.toHaveBeenCalled();
|
||||
|
@ -1191,7 +1293,8 @@ describe('Cases webhook service', () => {
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
const res = await service.createComment(commentReq);
|
||||
expect(requestMock).not.toHaveBeenCalled();
|
||||
|
@ -1217,7 +1320,8 @@ describe('Cases webhook service', () => {
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
await service.createComment(commentReq);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -1228,6 +1332,7 @@ describe('Cases webhook service', () => {
|
|||
url: 'https://coolsite.net/issue/1/comment',
|
||||
data: `{"body":"comment","id":"1"}`,
|
||||
sslOverrides: defaultSSLOverrides,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1257,7 +1362,8 @@ describe('Cases webhook service', () => {
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
await service.createComment(commentReq2);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -1268,6 +1374,7 @@ describe('Cases webhook service', () => {
|
|||
url: 'https://coolsite.net/issue/1/comment',
|
||||
data: `{"body":"comment","id":1}`,
|
||||
sslOverrides: defaultSSLOverrides,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1286,7 +1393,8 @@ describe('Cases webhook service', () => {
|
|||
ensureUriAllowed: jest.fn().mockImplementation(() => {
|
||||
throw new Error('Uri not allowed');
|
||||
}),
|
||||
}
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1360,7 +1468,8 @@ describe('Cases webhook service', () => {
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1430,7 +1539,8 @@ describe('Cases webhook service', () => {
|
|||
logger,
|
||||
{
|
||||
...configurationUtilities,
|
||||
}
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
requestMock.mockImplementation(() =>
|
||||
createAxiosResponse({
|
||||
|
|
|
@ -12,6 +12,7 @@ import { renderMustacheStringNoEscape } from '@kbn/actions-plugin/server/lib/mus
|
|||
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { buildConnectorAuth, validateConnectorAuthConfiguration } from '../../../common/auth/utils';
|
||||
import { validateAndNormalizeUrl, validateJson } from './validators';
|
||||
import {
|
||||
|
@ -38,7 +39,8 @@ export const createExternalService = (
|
|||
actionId: string,
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): ExternalService => {
|
||||
const {
|
||||
createCommentJson,
|
||||
|
@ -117,6 +119,7 @@ export const createExternalService = (
|
|||
logger,
|
||||
configurationUtilities,
|
||||
sslOverrides,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwDescriptiveErrorIfResponseIsNotValid({
|
||||
|
@ -162,6 +165,7 @@ export const createExternalService = (
|
|||
data: json,
|
||||
configurationUtilities,
|
||||
sslOverrides,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const { status, statusText, data } = res;
|
||||
|
@ -246,6 +250,7 @@ export const createExternalService = (
|
|||
data: json,
|
||||
configurationUtilities,
|
||||
sslOverrides,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwDescriptiveErrorIfResponseIsNotValid({
|
||||
|
@ -319,6 +324,7 @@ export const createExternalService = (
|
|||
data: json,
|
||||
configurationUtilities,
|
||||
sslOverrides,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwDescriptiveErrorIfResponseIsNotValid({
|
||||
|
|
|
@ -10,27 +10,34 @@ import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.moc
|
|||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { CROWDSTRIKE_CONNECTOR_ID } from '../../../public/common';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const tokenPath = 'https://api.crowdstrike.com/oauth2/token';
|
||||
const hostPath = 'https://api.crowdstrike.com/devices/entities/devices/v2';
|
||||
const onlineStatusPath = 'https://api.crowdstrike.com/devices/entities/online-state/v1';
|
||||
const actionsPath = 'https://api.crowdstrike.com/devices/entities/devices-actions/v2';
|
||||
describe('CrowdstrikeConnector', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const connector = new CrowdstrikeConnector({
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
connector: { id: '1', type: CROWDSTRIKE_CONNECTOR_ID },
|
||||
config: { url: 'https://api.crowdstrike.com' },
|
||||
secrets: { clientId: '123', clientSecret: 'secret' },
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
let mockedRequest: jest.Mock;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error private static - but I still want to reset it
|
||||
CrowdstrikeConnector.token = null;
|
||||
// @ts-expect-error
|
||||
mockedRequest = connector.request = jest.fn() as jest.Mock;
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -43,10 +50,13 @@ describe('CrowdstrikeConnector', () => {
|
|||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await connector.executeHostActions({
|
||||
command: 'contain',
|
||||
ids: ['id1', 'id2'],
|
||||
});
|
||||
const result = await connector.executeHostActions(
|
||||
{
|
||||
command: 'contain',
|
||||
ids: ['id1', 'id2'],
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
|
@ -58,7 +68,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
method: 'post',
|
||||
responseSchema: expect.any(Object),
|
||||
url: tokenPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
|
@ -69,7 +80,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
data: { ids: ['id1', 'id2'] },
|
||||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toEqual({ id: 'testid', path: 'testpath' });
|
||||
});
|
||||
|
@ -82,7 +94,10 @@ describe('CrowdstrikeConnector', () => {
|
|||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await connector.getAgentDetails({ ids: ['id1', 'id2'] });
|
||||
const result = await connector.getAgentDetails(
|
||||
{ ids: ['id1', 'id2'] },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
|
@ -95,7 +110,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
method: 'post',
|
||||
responseSchema: expect.any(Object),
|
||||
url: tokenPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
|
@ -108,7 +124,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
url: hostPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toEqual({ resources: [{}] });
|
||||
});
|
||||
|
@ -121,7 +138,10 @@ describe('CrowdstrikeConnector', () => {
|
|||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await connector.getAgentOnlineStatus({ ids: ['id1', 'id2'] });
|
||||
const result = await connector.getAgentOnlineStatus(
|
||||
{ ids: ['id1', 'id2'] },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
|
@ -134,7 +154,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
method: 'post',
|
||||
responseSchema: expect.any(Object),
|
||||
url: tokenPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
|
@ -147,7 +168,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
url: onlineStatusPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toEqual({ resources: [{}] });
|
||||
});
|
||||
|
@ -226,7 +248,7 @@ describe('CrowdstrikeConnector', () => {
|
|||
mockedRequest.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
// @ts-expect-error private method - but I still want to
|
||||
const result = await connector.getTokenRequest();
|
||||
const result = await connector.getTokenRequest(connectorUsageCollector);
|
||||
|
||||
expect(mockedRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -237,7 +259,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: expect.stringContaining('Basic'),
|
||||
},
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toEqual('testToken');
|
||||
});
|
||||
|
@ -247,7 +270,7 @@ describe('CrowdstrikeConnector', () => {
|
|||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
await connector.getAgentDetails({ ids: ['id1', 'id2'] });
|
||||
await connector.getAgentDetails({ ids: ['id1', 'id2'] }, connectorUsageCollector);
|
||||
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
|
@ -260,7 +283,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
method: 'post',
|
||||
responseSchema: expect.any(Object),
|
||||
url: tokenPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
|
@ -273,10 +297,11 @@ describe('CrowdstrikeConnector', () => {
|
|||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
url: hostPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(2);
|
||||
await connector.getAgentDetails({ ids: ['id1', 'id2'] });
|
||||
await connector.getAgentDetails({ ids: ['id1', 'id2'] }, connectorUsageCollector);
|
||||
expect(mockedRequest).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
|
@ -288,7 +313,8 @@ describe('CrowdstrikeConnector', () => {
|
|||
paramsSerializer: expect.any(Function),
|
||||
responseSchema: expect.any(Object),
|
||||
url: hostPath,
|
||||
})
|
||||
}),
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
@ -298,9 +324,9 @@ describe('CrowdstrikeConnector', () => {
|
|||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockRejectedValueOnce(mockResponse);
|
||||
|
||||
await expect(() => connector.getAgentDetails({ ids: ['id1', 'id2'] })).rejects.toThrowError(
|
||||
'something goes wrong'
|
||||
);
|
||||
await expect(() =>
|
||||
connector.getAgentDetails({ ids: ['id1', 'id2'] }, connectorUsageCollector)
|
||||
).rejects.toThrowError('something goes wrong');
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should repeat the call one time if theres 401 error ', async () => {
|
||||
|
@ -309,7 +335,9 @@ describe('CrowdstrikeConnector', () => {
|
|||
mockedRequest.mockResolvedValueOnce({ data: { access_token: 'testToken' } });
|
||||
mockedRequest.mockRejectedValueOnce(mockResponse);
|
||||
|
||||
await expect(() => connector.getAgentDetails({ ids: ['id1', 'id2'] })).rejects.toThrowError();
|
||||
await expect(() =>
|
||||
connector.getAgentDetails({ ids: ['id1', 'id2'] }, connectorUsageCollector)
|
||||
).rejects.toThrowError();
|
||||
expect(mockedRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { isAggregateError, NodeSystemError } from './types';
|
||||
import type {
|
||||
CrowdstrikeConfig,
|
||||
|
@ -96,68 +97,87 @@ export class CrowdstrikeConnector extends SubActionConnector<
|
|||
});
|
||||
}
|
||||
|
||||
public async executeHostActions({ alertIds, ...payload }: CrowdstrikeHostActionsParams) {
|
||||
return this.crowdstrikeApiRequest({
|
||||
url: this.urls.hostAction,
|
||||
method: 'post',
|
||||
params: {
|
||||
action_name: payload.command,
|
||||
public async executeHostActions(
|
||||
{ alertIds, ...payload }: CrowdstrikeHostActionsParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
return this.crowdstrikeApiRequest(
|
||||
{
|
||||
url: this.urls.hostAction,
|
||||
method: 'post',
|
||||
params: {
|
||||
action_name: payload.command,
|
||||
},
|
||||
data: {
|
||||
ids: payload.ids,
|
||||
...(payload.actionParameters
|
||||
? {
|
||||
action_parameters: Object.entries(payload.actionParameters).map(
|
||||
([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
})
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: CrowdstrikeHostActionsResponseSchema,
|
||||
},
|
||||
data: {
|
||||
ids: payload.ids,
|
||||
...(payload.actionParameters
|
||||
? {
|
||||
action_parameters: Object.entries(payload.actionParameters).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: CrowdstrikeHostActionsResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async getAgentDetails(
|
||||
payload: CrowdstrikeGetAgentsParams
|
||||
payload: CrowdstrikeGetAgentsParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<CrowdstrikeGetAgentsResponse> {
|
||||
return this.crowdstrikeApiRequest({
|
||||
url: this.urls.agents,
|
||||
method: 'GET',
|
||||
params: {
|
||||
ids: payload.ids,
|
||||
return this.crowdstrikeApiRequest(
|
||||
{
|
||||
url: this.urls.agents,
|
||||
method: 'GET',
|
||||
params: {
|
||||
ids: payload.ids,
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: RelaxedCrowdstrikeBaseApiResponseSchema,
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: RelaxedCrowdstrikeBaseApiResponseSchema,
|
||||
}) as Promise<CrowdstrikeGetAgentsResponse>;
|
||||
connectorUsageCollector
|
||||
) as Promise<CrowdstrikeGetAgentsResponse>;
|
||||
}
|
||||
|
||||
public async getAgentOnlineStatus(
|
||||
payload: CrowdstrikeGetAgentsParams
|
||||
payload: CrowdstrikeGetAgentsParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<CrowdstrikeGetAgentOnlineStatusResponse> {
|
||||
return this.crowdstrikeApiRequest({
|
||||
url: this.urls.agentStatus,
|
||||
method: 'GET',
|
||||
params: {
|
||||
ids: payload.ids,
|
||||
return this.crowdstrikeApiRequest(
|
||||
{
|
||||
url: this.urls.agentStatus,
|
||||
method: 'GET',
|
||||
params: {
|
||||
ids: payload.ids,
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: RelaxedCrowdstrikeBaseApiResponseSchema,
|
||||
},
|
||||
paramsSerializer,
|
||||
responseSchema: RelaxedCrowdstrikeBaseApiResponseSchema,
|
||||
}) as Promise<CrowdstrikeGetAgentOnlineStatusResponse>;
|
||||
connectorUsageCollector
|
||||
) as Promise<CrowdstrikeGetAgentOnlineStatusResponse>;
|
||||
}
|
||||
|
||||
private async getTokenRequest() {
|
||||
const response = await this.request<CrowdstrikeGetTokenResponse>({
|
||||
url: this.urls.getToken,
|
||||
method: 'post',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: 'Basic ' + CrowdstrikeConnector.base64encodedToken,
|
||||
private async getTokenRequest(connectorUsageCollector: ConnectorUsageCollector) {
|
||||
const response = await this.request<CrowdstrikeGetTokenResponse>(
|
||||
{
|
||||
url: this.urls.getToken,
|
||||
method: 'post',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
authorization: 'Basic ' + CrowdstrikeConnector.base64encodedToken,
|
||||
},
|
||||
responseSchema: CrowdstrikeGetTokenResponseSchema,
|
||||
},
|
||||
responseSchema: CrowdstrikeGetTokenResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
const token = response.data?.access_token;
|
||||
if (token) {
|
||||
// Clear any existing timeout
|
||||
|
@ -173,28 +193,33 @@ export class CrowdstrikeConnector extends SubActionConnector<
|
|||
|
||||
private async crowdstrikeApiRequest<R extends RelaxedCrowdstrikeBaseApiResponse>(
|
||||
req: SubActionRequestParams<R>,
|
||||
connectorUsageCollector: ConnectorUsageCollector,
|
||||
retried?: boolean
|
||||
): Promise<R> {
|
||||
try {
|
||||
if (!CrowdstrikeConnector.token) {
|
||||
CrowdstrikeConnector.token = (await this.getTokenRequest()) as string;
|
||||
CrowdstrikeConnector.token = (await this.getTokenRequest(
|
||||
connectorUsageCollector
|
||||
)) as string;
|
||||
}
|
||||
|
||||
const response = await this.request<R>({
|
||||
...req,
|
||||
headers: {
|
||||
...req.headers,
|
||||
Authorization: `Bearer ${CrowdstrikeConnector.token}`,
|
||||
const response = await this.request<R>(
|
||||
{
|
||||
...req,
|
||||
headers: {
|
||||
...req.headers,
|
||||
Authorization: `Bearer ${CrowdstrikeConnector.token}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.code === 401 && !retried) {
|
||||
CrowdstrikeConnector.token = null;
|
||||
return this.crowdstrikeApiRequest(req, true);
|
||||
return this.crowdstrikeApiRequest(req, connectorUsageCollector, true);
|
||||
}
|
||||
|
||||
throw new CrowdstrikeError(error.message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { D3_SECURITY_CONNECTOR_ID } from '../../../common/d3security/constants';
|
|||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { D3SecurityRunActionResponseSchema } from '../../../common/d3security/schema';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
describe('D3SecurityConnector', () => {
|
||||
const sampleBody = JSON.stringify({
|
||||
|
@ -28,6 +29,7 @@ describe('D3SecurityConnector', () => {
|
|||
const mockError = jest.fn().mockImplementation(() => {
|
||||
throw new Error('API Error');
|
||||
});
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
describe('D3 Security', () => {
|
||||
const connector = new D3SecurityConnector({
|
||||
|
@ -35,26 +37,35 @@ describe('D3SecurityConnector', () => {
|
|||
connector: { id: '1', type: D3_SECURITY_CONNECTOR_ID },
|
||||
config: { url: 'https://example.com/api' },
|
||||
secrets: { token: '123' },
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
connector.request = mockRequest;
|
||||
jest.clearAllMocks();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
it('the D3 Security API call is successful with correct parameters', async () => {
|
||||
const response = await connector.runApi({ body: sampleBody });
|
||||
const response = await connector.runApi({ body: sampleBody }, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/api',
|
||||
method: 'post',
|
||||
responseSchema: D3SecurityRunActionResponseSchema,
|
||||
data: sampleBodyFormatted,
|
||||
headers: {
|
||||
d3key: '123',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://example.com/api',
|
||||
method: 'post',
|
||||
responseSchema: D3SecurityRunActionResponseSchema,
|
||||
data: sampleBodyFormatted,
|
||||
headers: {
|
||||
d3key: '123',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({ result: 'success' });
|
||||
});
|
||||
|
||||
|
@ -62,7 +73,9 @@ describe('D3SecurityConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.runApi({ body: sampleBody })).rejects.toThrow('API Error');
|
||||
await expect(connector.runApi({ body: sampleBody }, connectorUsageCollector)).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { addSeverityAndEventTypeInBody } from './helpers';
|
||||
import {
|
||||
D3SecurityRunActionParamsSchema,
|
||||
|
@ -57,22 +58,24 @@ export class D3SecurityConnector extends SubActionConnector<D3SecurityConfig, D3
|
|||
return `API Error: ${error.response?.status} - ${error.response?.statusText}`;
|
||||
}
|
||||
|
||||
public async runApi({
|
||||
body,
|
||||
severity,
|
||||
eventType,
|
||||
}: D3SecurityRunActionParams): Promise<D3SecurityRunActionResponse> {
|
||||
const response = await this.request({
|
||||
url: this.url,
|
||||
method: 'post',
|
||||
responseSchema: D3SecurityRunActionResponseSchema,
|
||||
data: addSeverityAndEventTypeInBody(
|
||||
body ?? '',
|
||||
severity ?? D3SecuritySeverity.EMPTY,
|
||||
eventType ?? ''
|
||||
),
|
||||
headers: { d3key: this.token || '' },
|
||||
});
|
||||
public async runApi(
|
||||
{ body, severity, eventType }: D3SecurityRunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<D3SecurityRunActionResponse> {
|
||||
const response = await this.request(
|
||||
{
|
||||
url: this.url,
|
||||
method: 'post',
|
||||
responseSchema: D3SecurityRunActionResponseSchema,
|
||||
data: addSeverityAndEventTypeInBody(
|
||||
body ?? '',
|
||||
severity ?? D3SecuritySeverity.EMPTY,
|
||||
eventType ?? ''
|
||||
),
|
||||
headers: { d3key: this.token || '' },
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import {
|
|||
validateParams,
|
||||
validateSecrets,
|
||||
} from '@kbn/actions-plugin/server/lib';
|
||||
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { sendEmail } from './send_email';
|
||||
import {
|
||||
ActionParamsType,
|
||||
|
@ -514,6 +516,10 @@ describe('execute()', () => {
|
|||
text: 'Go to Elastic',
|
||||
},
|
||||
};
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
|
||||
const actionId = 'some-id';
|
||||
const executorOptions: EmailConnectorTypeExecutorOptions = {
|
||||
|
@ -524,6 +530,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -274,8 +274,16 @@ async function executor(
|
|||
},
|
||||
execOptions: EmailConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> {
|
||||
const { actionId, config, secrets, params, configurationUtilities, services, logger } =
|
||||
execOptions;
|
||||
const {
|
||||
actionId,
|
||||
config,
|
||||
secrets,
|
||||
params,
|
||||
configurationUtilities,
|
||||
services,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
} = execOptions;
|
||||
const connectorTokenClient = services.connectorTokenClient;
|
||||
|
||||
const emails = params.to.concat(params.cc).concat(params.bcc);
|
||||
|
@ -366,7 +374,12 @@ async function executor(
|
|||
let result;
|
||||
|
||||
try {
|
||||
result = await sendEmail(logger, sendEmailOptions, connectorTokenClient);
|
||||
result = await sendEmail(
|
||||
logger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
} catch (err) {
|
||||
const message = i18n.translate('xpack.stackConnectors.email.errorSendingErrorMessage', {
|
||||
defaultMessage: 'error sending email',
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Logger } from '@kbn/core/server';
|
|||
import { sendEmail } from './send_email';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { ProxySettings } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, ProxySettings } from '@kbn/actions-plugin/server/types';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { CustomHostSettings } from '@kbn/actions-plugin/server/config';
|
||||
import { sendEmailGraphApi } from './send_email_graph_api';
|
||||
|
@ -39,6 +39,7 @@ const sendMailMock = jest.fn();
|
|||
const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
const connectorTokenClient = connectorTokenClientMock.create();
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
describe('send_email module', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -53,11 +54,21 @@ describe('send_email module', () => {
|
|||
interceptors: mockAxiosInstanceInterceptor,
|
||||
};
|
||||
});
|
||||
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
test('handles authenticated email using service', async () => {
|
||||
const sendEmailOptions = getSendEmailOptions({ transport: { service: 'other' } });
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -101,7 +112,12 @@ describe('send_email module', () => {
|
|||
content: { hasHTMLMessage: true },
|
||||
transport: { service: 'other' },
|
||||
});
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -159,7 +175,7 @@ describe('send_email module', () => {
|
|||
status: 202,
|
||||
});
|
||||
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient, connectorUsageCollector);
|
||||
expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({
|
||||
configurationUtilities: sendEmailOptions.configurationUtilities,
|
||||
connectorId: '1',
|
||||
|
@ -176,6 +192,7 @@ describe('send_email module', () => {
|
|||
delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
|
||||
sendEmailGraphApiMock.mock.calls[0].pop();
|
||||
sendEmailGraphApiMock.mock.calls[0].pop();
|
||||
sendEmailGraphApiMock.mock.calls[0].pop();
|
||||
expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
|
@ -254,7 +271,7 @@ describe('send_email module', () => {
|
|||
status: 202,
|
||||
});
|
||||
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient, connectorUsageCollector);
|
||||
expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({
|
||||
configurationUtilities: sendEmailOptions.configurationUtilities,
|
||||
connectorId: '1',
|
||||
|
@ -292,7 +309,7 @@ describe('send_email module', () => {
|
|||
status: 202,
|
||||
});
|
||||
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient, connectorUsageCollector);
|
||||
expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({
|
||||
configurationUtilities: sendEmailOptions.configurationUtilities,
|
||||
connectorId: '1',
|
||||
|
@ -322,7 +339,7 @@ describe('send_email module', () => {
|
|||
getOAuthClientCredentialsAccessTokenMock.mockReturnValueOnce(null);
|
||||
|
||||
await expect(() =>
|
||||
sendEmail(mockLogger, sendEmailOptions, connectorTokenClient)
|
||||
sendEmail(mockLogger, sendEmailOptions, connectorTokenClient, connectorUsageCollector)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to retrieve access token for connectorId: 1"`
|
||||
);
|
||||
|
@ -362,7 +379,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -412,7 +434,12 @@ describe('send_email module', () => {
|
|||
delete sendEmailOptions.transport.user;
|
||||
// @ts-expect-error
|
||||
delete sendEmailOptions.transport.password;
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -462,7 +489,12 @@ describe('send_email module', () => {
|
|||
// @ts-expect-error
|
||||
delete sendEmailOptions.transport.password;
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -503,9 +535,9 @@ describe('send_email module', () => {
|
|||
sendMailMock.mockReset();
|
||||
sendMailMock.mockRejectedValue(new Error('wops'));
|
||||
|
||||
await expect(sendEmail(mockLogger, sendEmailOptions, connectorTokenClient)).rejects.toThrow(
|
||||
'wops'
|
||||
);
|
||||
await expect(
|
||||
sendEmail(mockLogger, sendEmailOptions, connectorTokenClient, connectorUsageCollector)
|
||||
).rejects.toThrow('wops');
|
||||
});
|
||||
|
||||
test('it bypasses with proxyBypassHosts when expected', async () => {
|
||||
|
@ -526,7 +558,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -560,7 +597,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -596,7 +638,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -630,7 +677,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -667,7 +719,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
|
||||
// note in the object below, the rejectUnauthenticated got set to false,
|
||||
|
@ -710,7 +767,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
|
||||
// in this case, rejectUnauthorized is true, as the custom host settings
|
||||
|
@ -757,7 +819,12 @@ describe('send_email module', () => {
|
|||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
const result = await sendEmail(
|
||||
mockLogger,
|
||||
sendEmailOptions,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -791,7 +858,7 @@ describe('send_email module', () => {
|
|||
'Bearer clienttokentokentoken'
|
||||
);
|
||||
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient, connectorUsageCollector);
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1);
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledWith();
|
||||
expect(mockAxiosInstanceInterceptor.response.use).toHaveBeenCalledTimes(1);
|
||||
|
@ -834,7 +901,7 @@ describe('send_email module', () => {
|
|||
'Bearer clienttokentokentoken'
|
||||
);
|
||||
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
|
||||
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient, connectorUsageCollector);
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1);
|
||||
expect(createAxiosInstanceMock).toHaveBeenCalledWith();
|
||||
expect(mockAxiosInstanceInterceptor.response.use).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -17,7 +17,11 @@ import {
|
|||
getNodeSSLOptions,
|
||||
getSSLSettingsFromConfig,
|
||||
} from '@kbn/actions-plugin/server/lib/get_node_ssl_options';
|
||||
import { ConnectorTokenClientContract, ProxySettings } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
ConnectorUsageCollector,
|
||||
ConnectorTokenClientContract,
|
||||
ProxySettings,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
import { getOAuthClientCredentialsAccessToken } from '@kbn/actions-plugin/server/lib/get_oauth_client_credentials_access_token';
|
||||
import { AdditionalEmailServices } from '../../../common';
|
||||
import { sendEmailGraphApi } from './send_email_graph_api';
|
||||
|
@ -66,7 +70,8 @@ export interface Content {
|
|||
export async function sendEmail(
|
||||
logger: Logger,
|
||||
options: SendEmailOptions,
|
||||
connectorTokenClient: ConnectorTokenClientContract
|
||||
connectorTokenClient: ConnectorTokenClientContract,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<unknown> {
|
||||
const { transport, content } = options;
|
||||
const { message, messageHTML } = content;
|
||||
|
@ -74,9 +79,15 @@ export async function sendEmail(
|
|||
const renderedMessage = messageHTML ?? htmlFromMarkdown(logger, message);
|
||||
|
||||
if (transport.service === AdditionalEmailServices.EXCHANGE) {
|
||||
return await sendEmailWithExchange(logger, options, renderedMessage, connectorTokenClient);
|
||||
return await sendEmailWithExchange(
|
||||
logger,
|
||||
options,
|
||||
renderedMessage,
|
||||
connectorTokenClient,
|
||||
connectorUsageCollector
|
||||
);
|
||||
} else {
|
||||
return await sendEmailWithNodemailer(logger, options, renderedMessage);
|
||||
return await sendEmailWithNodemailer(logger, options, renderedMessage, connectorUsageCollector);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +96,8 @@ export async function sendEmailWithExchange(
|
|||
logger: Logger,
|
||||
options: SendEmailOptions,
|
||||
messageHTML: string,
|
||||
connectorTokenClient: ConnectorTokenClientContract
|
||||
connectorTokenClient: ConnectorTokenClientContract,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<unknown> {
|
||||
const { transport, configurationUtilities, connectorId } = options;
|
||||
const { clientId, clientSecret, tenantId, oauthTokenUrl } = transport;
|
||||
|
@ -155,6 +167,7 @@ export async function sendEmailWithExchange(
|
|||
},
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
axiosInstance
|
||||
);
|
||||
}
|
||||
|
@ -163,7 +176,8 @@ export async function sendEmailWithExchange(
|
|||
async function sendEmailWithNodemailer(
|
||||
logger: Logger,
|
||||
options: SendEmailOptions,
|
||||
messageHTML: string
|
||||
messageHTML: string,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<unknown> {
|
||||
const { transport, routing, content, configurationUtilities, hasAuth } = options;
|
||||
const { service } = transport;
|
||||
|
@ -186,6 +200,7 @@ async function sendEmailWithNodemailer(
|
|||
// some deep properties, so need to use any here.
|
||||
const transportConfig = getTransportConfig(configurationUtilities, logger, transport, hasAuth);
|
||||
const nodemailerTransport = nodemailer.createTransport(transportConfig);
|
||||
connectorUsageCollector.addRequestBodyBytes(undefined, email);
|
||||
const result = await nodemailerTransport.sendMail(email);
|
||||
|
||||
if (service === JSON_TRANSPORT_SERVICE) {
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Logger } from '@kbn/core/server';
|
|||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { CustomHostSettings } from '@kbn/actions-plugin/server/config';
|
||||
import { ProxySettings } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, ProxySettings } from '@kbn/actions-plugin/server/types';
|
||||
import { sendEmailGraphApi } from './send_email_graph_api';
|
||||
|
||||
const createAxiosInstanceMock = axios.create as jest.Mock;
|
||||
|
@ -28,6 +28,11 @@ describe('sendEmailGraphApi', () => {
|
|||
const configurationUtilities = actionsConfigMock.create();
|
||||
|
||||
test('email contains the proper message', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
|
||||
axiosInstanceMock.mockReturnValueOnce({
|
||||
status: 202,
|
||||
});
|
||||
|
@ -38,7 +43,8 @@ describe('sendEmailGraphApi', () => {
|
|||
headers: {},
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(axiosInstanceMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -118,6 +124,10 @@ describe('sendEmailGraphApi', () => {
|
|||
});
|
||||
|
||||
test('email was sent on behalf of the user "from" mailbox', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
axiosInstanceMock.mockReturnValueOnce({
|
||||
status: 202,
|
||||
});
|
||||
|
@ -128,7 +138,8 @@ describe('sendEmailGraphApi', () => {
|
|||
headers: { Authorization: 'Bearer 1234567' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(axiosInstanceMock.mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -210,6 +221,10 @@ describe('sendEmailGraphApi', () => {
|
|||
});
|
||||
|
||||
test('sendMail request was sent to the custom configured Graph API URL', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
axiosInstanceMock.mockReturnValueOnce({
|
||||
status: 202,
|
||||
});
|
||||
|
@ -221,7 +236,8 @@ describe('sendEmailGraphApi', () => {
|
|||
headers: {},
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(axiosInstanceMock.mock.calls[2]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -301,6 +317,10 @@ describe('sendEmailGraphApi', () => {
|
|||
});
|
||||
|
||||
test('throw the exception and log the proper error if message was not sent successfuly', async () => {
|
||||
const connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
axiosInstanceMock.mockReturnValueOnce({
|
||||
status: 400,
|
||||
data: {
|
||||
|
@ -315,7 +335,8 @@ describe('sendEmailGraphApi', () => {
|
|||
sendEmailGraphApi(
|
||||
{ options: getSendEmailOptions(), messageHTML: 'test1', headers: {} },
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
'"{\\"error\\":{\\"code\\":\\"ErrorMimeContentInvalidBase64String\\",\\"message\\":\\"Invalid base64 string for MIME content.\\"}}"'
|
||||
|
|
|
@ -11,18 +11,14 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
|||
import { Logger } from '@kbn/core/server';
|
||||
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { SendEmailOptions } from './send_email';
|
||||
|
||||
interface SendEmailGraphApiOptions {
|
||||
options: SendEmailOptions;
|
||||
headers: Record<string, string>;
|
||||
messageHTML: string;
|
||||
}
|
||||
|
||||
export async function sendEmailGraphApi(
|
||||
sendEmailOptions: SendEmailGraphApiOptions,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
connectorUsageCollector: ConnectorUsageCollector,
|
||||
axiosInstance?: AxiosInstance
|
||||
): Promise<AxiosResponse> {
|
||||
const { options, headers, messageHTML } = sendEmailOptions;
|
||||
|
@ -42,6 +38,7 @@ export async function sendEmailGraphApi(
|
|||
headers,
|
||||
configurationUtilities,
|
||||
validateStatus: () => true,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
if (res.status === 202) {
|
||||
return res.data;
|
||||
|
@ -53,6 +50,12 @@ export async function sendEmailGraphApi(
|
|||
throw new Error(errString);
|
||||
}
|
||||
|
||||
interface SendEmailGraphApiOptions {
|
||||
options: SendEmailOptions;
|
||||
headers: Record<string, string>;
|
||||
messageHTML: string;
|
||||
}
|
||||
|
||||
function getMessage(emailOptions: SendEmailOptions, messageHTML: string) {
|
||||
const { routing, content } = emailOptions;
|
||||
const { to, cc, bcc } = routing;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { validateConfig, validateParams } from '@kbn/actions-plugin/server/lib';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import {
|
||||
ActionParamsType,
|
||||
|
@ -27,11 +28,16 @@ const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
|||
|
||||
let connectorType: ESIndexConnectorType;
|
||||
let configurationUtilities: ActionsConfigurationUtilities;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('connector registration', () => {
|
||||
|
@ -185,6 +191,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const scopedClusterClient = elasticsearchClientMock
|
||||
.createClusterClient()
|
||||
|
@ -230,6 +237,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
scopedClusterClient.bulk.mockClear();
|
||||
await connectorType.executor({
|
||||
|
@ -280,6 +288,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
|
||||
scopedClusterClient.bulk.mockClear();
|
||||
|
@ -324,6 +333,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
scopedClusterClient.bulk.mockClear();
|
||||
await connectorType.executor({
|
||||
|
@ -656,6 +666,7 @@ describe('execute()', () => {
|
|||
services: { ...services, scopedClusterClient },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -695,6 +706,7 @@ describe('execute()', () => {
|
|||
services: { ...services, scopedClusterClient },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -757,6 +769,7 @@ describe('execute()', () => {
|
|||
services: { ...services, scopedClusterClient },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -824,6 +837,7 @@ describe('execute()', () => {
|
|||
services: { ...services, scopedClusterClient },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { RunApiResponseSchema, StreamingResponseSchema } from '../../../common/g
|
|||
import { DEFAULT_GEMINI_MODEL } from '../../../common/gemini/constants';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Transform } from 'stream';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
jest.mock('../lib/gen_ai/create_gen_ai_dashboard');
|
||||
jest.mock('@kbn/actions-plugin/server/sub_action_framework/helpers/validators', () => ({
|
||||
|
@ -61,6 +62,7 @@ describe('GeminiConnector', () => {
|
|||
mockRequest = connector.request = jest.fn().mockResolvedValue(defaultResponse);
|
||||
});
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const connector = new GeminiConnector({
|
||||
connector: { id: '1', type: '.gemini' },
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
|
@ -84,14 +86,19 @@ describe('GeminiConnector', () => {
|
|||
client_x509_cert_url: '',
|
||||
}),
|
||||
},
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
describe('Gemini', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
connector.request = mockRequest;
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('runApi', () => {
|
||||
|
@ -101,33 +108,36 @@ describe('GeminiConnector', () => {
|
|||
model: DEFAULT_GEMINI_MODEL,
|
||||
};
|
||||
|
||||
const response = await connector.runApi(runActionParams);
|
||||
const response = await connector.runApi(runActionParams, connectorUsageCollector);
|
||||
|
||||
// Assertions
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:generateContent`,
|
||||
method: 'post',
|
||||
data: JSON.stringify({
|
||||
messages: [
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:generateContent`,
|
||||
method: 'post',
|
||||
data: JSON.stringify({
|
||||
messages: [
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 60000,
|
||||
responseSchema: RunApiResponseSchema,
|
||||
signal: undefined,
|
||||
},
|
||||
timeout: 60000,
|
||||
responseSchema: RunApiResponseSchema,
|
||||
signal: undefined,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(response).toEqual(connectorResponse);
|
||||
});
|
||||
|
@ -144,66 +154,72 @@ describe('GeminiConnector', () => {
|
|||
};
|
||||
|
||||
it('the API call is successful with correct parameters', async () => {
|
||||
await connector.invokeAI(aiAssistantBody);
|
||||
await connector.invokeAI(aiAssistantBody, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:generateContent`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:generateContent`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
signal: undefined,
|
||||
timeout: 60000,
|
||||
},
|
||||
signal: undefined,
|
||||
timeout: 60000,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('signal and timeout is properly passed to runApi', async () => {
|
||||
const signal = jest.fn();
|
||||
const timeout = 60000;
|
||||
await connector.invokeAI({ ...aiAssistantBody, timeout, signal });
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:generateContent`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
await connector.invokeAI({ ...aiAssistantBody, timeout, signal }, connectorUsageCollector);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:generateContent`,
|
||||
method: 'post',
|
||||
responseSchema: RunApiResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
signal,
|
||||
timeout: 60000,
|
||||
},
|
||||
signal,
|
||||
timeout: 60000,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -226,68 +242,77 @@ describe('GeminiConnector', () => {
|
|||
};
|
||||
|
||||
it('the API call is successful with correct request parameters', async () => {
|
||||
await connector.invokeStream(aiAssistantBody);
|
||||
await connector.invokeStream(aiAssistantBody, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:streamGenerateContent?alt=sse`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:streamGenerateContent?alt=sse`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
signal: undefined,
|
||||
timeout: 60000,
|
||||
},
|
||||
signal: undefined,
|
||||
timeout: 60000,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('signal and timeout is properly passed to streamApi', async () => {
|
||||
const signal = jest.fn();
|
||||
const timeout = 60000;
|
||||
await connector.invokeStream({ ...aiAssistantBody, timeout, signal });
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:streamGenerateContent?alt=sse`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
await connector.invokeStream(
|
||||
{ ...aiAssistantBody, timeout, signal },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: `https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:streamGenerateContent?alt=sse`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192,
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
safety_settings: [
|
||||
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
||||
],
|
||||
}),
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
signal,
|
||||
timeout: 60000,
|
||||
},
|
||||
signal,
|
||||
timeout: 60000,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,10 @@ import { PassThrough } from 'stream';
|
|||
import { IncomingMessage } from 'http';
|
||||
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { getGoogleOAuthJwtAccessToken } from '@kbn/actions-plugin/server/lib/get_gcp_oauth_access_token';
|
||||
import { ConnectorTokenClientContract } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
ConnectorUsageCollector,
|
||||
ConnectorTokenClientContract,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
|
||||
import { HarmBlockThreshold, HarmCategory } from '@google/generative-ai';
|
||||
import {
|
||||
|
@ -211,13 +214,10 @@ export class GeminiConnector extends SubActionConnector<Config, Secrets> {
|
|||
* @param body The stringified request body to be sent in the POST request.
|
||||
* @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used.
|
||||
*/
|
||||
public async runApi({
|
||||
body,
|
||||
model: reqModel,
|
||||
signal,
|
||||
timeout,
|
||||
raw,
|
||||
}: RunActionParams): Promise<RunActionResponse | RunActionRawResponse> {
|
||||
public async runApi(
|
||||
{ body, model: reqModel, signal, timeout, raw }: RunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<RunActionResponse | RunActionRawResponse> {
|
||||
// set model on per request basis
|
||||
const currentModel = reqModel ?? this.model;
|
||||
const path = `/v1/projects/${this.gcpProjectID}/locations/${this.gcpRegion}/publishers/google/models/${currentModel}:generateContent`;
|
||||
|
@ -236,7 +236,7 @@ export class GeminiConnector extends SubActionConnector<Config, Secrets> {
|
|||
responseSchema: raw ? RunActionRawResponseSchema : RunApiResponseSchema,
|
||||
} as SubActionRequestParams<RunApiResponse>;
|
||||
|
||||
const response = await this.request(requestArgs);
|
||||
const response = await this.request(requestArgs, connectorUsageCollector);
|
||||
|
||||
if (raw) {
|
||||
return response.data;
|
||||
|
@ -249,65 +249,65 @@ export class GeminiConnector extends SubActionConnector<Config, Secrets> {
|
|||
return { completion: completionText, usageMetadata };
|
||||
}
|
||||
|
||||
private async streamAPI({
|
||||
body,
|
||||
model: reqModel,
|
||||
signal,
|
||||
timeout,
|
||||
}: RunActionParams): Promise<StreamingResponse> {
|
||||
private async streamAPI(
|
||||
{ body, model: reqModel, signal, timeout }: RunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<StreamingResponse> {
|
||||
const currentModel = reqModel ?? this.model;
|
||||
const path = `/v1/projects/${this.gcpProjectID}/locations/${this.gcpRegion}/publishers/google/models/${currentModel}:streamGenerateContent?alt=sse`;
|
||||
const token = await this.getAccessToken();
|
||||
|
||||
const response = await this.request({
|
||||
url: `${this.url}${path}`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: body,
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
const response = await this.request(
|
||||
{
|
||||
url: `${this.url}${path}`,
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: body,
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal,
|
||||
timeout: timeout ?? DEFAULT_TIMEOUT_MS,
|
||||
},
|
||||
signal,
|
||||
timeout: timeout ?? DEFAULT_TIMEOUT_MS,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return response.data.pipe(new PassThrough());
|
||||
}
|
||||
|
||||
public async invokeAI({
|
||||
messages,
|
||||
model,
|
||||
temperature = 0,
|
||||
signal,
|
||||
timeout,
|
||||
}: InvokeAIActionParams): Promise<InvokeAIActionResponse> {
|
||||
const res = await this.runApi({
|
||||
body: JSON.stringify(formatGeminiPayload(messages, temperature)),
|
||||
model,
|
||||
signal,
|
||||
timeout,
|
||||
});
|
||||
public async invokeAI(
|
||||
{ messages, model, temperature = 0, signal, timeout }: InvokeAIActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<InvokeAIActionResponse> {
|
||||
const res = await this.runApi(
|
||||
{
|
||||
body: JSON.stringify(formatGeminiPayload(messages, temperature)),
|
||||
model,
|
||||
signal,
|
||||
timeout,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return { message: res.completion, usageMetadata: res.usageMetadata };
|
||||
}
|
||||
|
||||
public async invokeAIRaw({
|
||||
messages,
|
||||
model,
|
||||
temperature = 0,
|
||||
signal,
|
||||
timeout,
|
||||
tools,
|
||||
}: InvokeAIRawActionParams): Promise<InvokeAIRawActionResponse> {
|
||||
const res = await this.runApi({
|
||||
body: JSON.stringify({ ...formatGeminiPayload(messages, temperature), tools }),
|
||||
model,
|
||||
signal,
|
||||
timeout,
|
||||
raw: true,
|
||||
});
|
||||
public async invokeAIRaw(
|
||||
{ messages, model, temperature = 0, signal, timeout, tools }: InvokeAIRawActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<InvokeAIRawActionResponse> {
|
||||
const res = await this.runApi(
|
||||
{
|
||||
body: JSON.stringify({ ...formatGeminiPayload(messages, temperature), tools }),
|
||||
model,
|
||||
signal,
|
||||
timeout,
|
||||
raw: true,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
@ -320,22 +320,28 @@ export class GeminiConnector extends SubActionConnector<Config, Secrets> {
|
|||
* @param messages An array of messages to be sent to the API
|
||||
* @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used.
|
||||
*/
|
||||
public async invokeStream({
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
temperature = 0,
|
||||
signal,
|
||||
timeout,
|
||||
tools,
|
||||
}: InvokeAIActionParams): Promise<IncomingMessage> {
|
||||
return (await this.streamAPI({
|
||||
body: JSON.stringify({ ...formatGeminiPayload(messages, temperature), tools }),
|
||||
public async invokeStream(
|
||||
{
|
||||
messages,
|
||||
model,
|
||||
stopSequences,
|
||||
temperature = 0,
|
||||
signal,
|
||||
timeout,
|
||||
})) as unknown as IncomingMessage;
|
||||
tools,
|
||||
}: InvokeAIActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<IncomingMessage> {
|
||||
return (await this.streamAPI(
|
||||
{
|
||||
body: JSON.stringify({ ...formatGeminiPayload(messages, temperature), tools }),
|
||||
model,
|
||||
stopSequences,
|
||||
signal,
|
||||
timeout,
|
||||
},
|
||||
connectorUsageCollector
|
||||
)) as unknown as IncomingMessage;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,15 @@ async function executor(
|
|||
ExecutorParams
|
||||
>
|
||||
): Promise<ConnectorTypeExecutorResult<JiraExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions;
|
||||
const {
|
||||
actionId,
|
||||
config,
|
||||
params,
|
||||
secrets,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
} = execOptions;
|
||||
const { subAction, subActionParams } = params as ExecutorParams;
|
||||
let data: JiraExecutorResultData | null = null;
|
||||
|
||||
|
@ -105,7 +113,8 @@ async function executor(
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { Logger } from '@kbn/core/server';
|
|||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { getBasicAuthHeader } from '@kbn/actions-plugin/server';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
interface ResponseError extends Error {
|
||||
|
@ -135,8 +136,13 @@ const mockOldAPI = () =>
|
|||
|
||||
describe('Jira service', () => {
|
||||
let service: ExternalService;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeAll(() => {
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
service = createExternalService(
|
||||
{
|
||||
// The trailing slash at the end of the url is intended.
|
||||
|
@ -145,7 +151,8 @@ describe('Jira service', () => {
|
|||
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -162,7 +169,8 @@ describe('Jira service', () => {
|
|||
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -175,7 +183,8 @@ describe('Jira service', () => {
|
|||
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -188,7 +197,8 @@ describe('Jira service', () => {
|
|||
secrets: { apiToken: 'token' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -201,7 +211,8 @@ describe('Jira service', () => {
|
|||
secrets: { email: 'elastic@elastic.com' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -213,7 +224,8 @@ describe('Jira service', () => {
|
|||
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(axios.create).toHaveBeenCalledWith({
|
||||
|
@ -258,6 +270,7 @@ describe('Jira service', () => {
|
|||
url: 'https://coolsite.net/rest/api/2/issue/1',
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -401,6 +414,7 @@ describe('Jira service', () => {
|
|||
priority: { name: 'High' },
|
||||
},
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -459,6 +473,7 @@ describe('Jira service', () => {
|
|||
priority: { name: 'High' },
|
||||
},
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -492,6 +507,7 @@ describe('Jira service', () => {
|
|||
parent: { key: 'RJ-107' },
|
||||
},
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -561,6 +577,7 @@ describe('Jira service', () => {
|
|||
...otherFields,
|
||||
},
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -631,6 +648,7 @@ describe('Jira service', () => {
|
|||
parent: { key: 'RJ-107' },
|
||||
},
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -693,6 +711,7 @@ describe('Jira service', () => {
|
|||
...otherFields,
|
||||
},
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -746,6 +765,7 @@ describe('Jira service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/1/comment',
|
||||
data: { body: 'comment' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -802,6 +822,7 @@ describe('Jira service', () => {
|
|||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/capabilities',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -883,6 +904,7 @@ describe('Jira service', () => {
|
|||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -957,6 +979,7 @@ describe('Jira service', () => {
|
|||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1032,6 +1055,7 @@ describe('Jira service', () => {
|
|||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1240,6 +1264,7 @@ describe('Jira service', () => {
|
|||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: `https://coolsite.net/rest/api/2/search?jql=project%3D%22CK%22%20and%20summary%20~%22Test%20title%22`,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1266,6 +1291,7 @@ describe('Jira service', () => {
|
|||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: `https://coolsite.net/rest/api/2/search?jql=project%3D%22CK%22%20and%20summary%20~%22%5C%5C%5Bth%5C%5C!s%5C%5C%5Eis%5C%5C(%5C%5C)a%5C%5C-te%5C%5C%2Bst%5C%5C-%5C%5C%7B%5C%5C~is%5C%5C*s%5C%5C%26ue%5C%5C%3For%5C%5C%7Cand%5C%5Cbye%5C%5C%3A%5C%5C%7D%5C%5C%5D%5C%5C%7D%5C%5C%5D%22`,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1344,6 +1370,7 @@ describe('Jira service', () => {
|
|||
method: 'get',
|
||||
configurationUtilities,
|
||||
url: `https://coolsite.net/rest/api/2/issue/RJ-107`,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { getBasicAuthHeader } from '@kbn/actions-plugin/server';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
CreateCommentParams,
|
||||
CreateIncidentParams,
|
||||
|
@ -47,7 +48,8 @@ const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-field
|
|||
export const createExternalService = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): ExternalService => {
|
||||
const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType;
|
||||
const { apiToken, email } = secrets as JiraSecretConfigurationType;
|
||||
|
@ -189,6 +191,7 @@ export const createExternalService = (
|
|||
url: `${incidentUrl}/${id}`,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -242,6 +245,7 @@ export const createExternalService = (
|
|||
fields,
|
||||
},
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -288,6 +292,7 @@ export const createExternalService = (
|
|||
logger,
|
||||
data: { fields },
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -326,6 +331,7 @@ export const createExternalService = (
|
|||
logger,
|
||||
data: { body: comment.comment },
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -358,6 +364,7 @@ export const createExternalService = (
|
|||
url: capabilitiesUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -389,6 +396,7 @@ export const createExternalService = (
|
|||
url: getIssueTypesOldAPIURL,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -404,6 +412,7 @@ export const createExternalService = (
|
|||
url: getIssueTypesUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -436,6 +445,7 @@ export const createExternalService = (
|
|||
url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId),
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -515,6 +525,7 @@ export const createExternalService = (
|
|||
url: query,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -543,6 +554,7 @@ export const createExternalService = (
|
|||
url: getIssueUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
|
|
@ -12,6 +12,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks';
|
|||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { connectorTokenClientMock } from '@kbn/actions-plugin/server/lib/connector_token_client.mock';
|
||||
import { snExternalServiceConfig } from './config';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
const connectorTokenClient = connectorTokenClientMock.create();
|
||||
|
@ -19,10 +20,15 @@ const configurationUtilities = actionsConfigMock.create();
|
|||
|
||||
jest.mock('axios');
|
||||
axios.create = jest.fn(() => axios);
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
describe('createServiceWrapper', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
test('creates axios instance with apiUrl', () => {
|
||||
|
@ -45,6 +51,7 @@ describe('createServiceWrapper', () => {
|
|||
serviceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(createServiceFn).toHaveBeenCalledWith({
|
||||
|
@ -53,6 +60,7 @@ describe('createServiceWrapper', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -76,6 +84,7 @@ describe('createServiceWrapper', () => {
|
|||
serviceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(createServiceFn).toHaveBeenCalledWith({
|
||||
|
@ -84,6 +93,7 @@ describe('createServiceWrapper', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import type { ConnectorTokenClientContract } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
ConnectorUsageCollector,
|
||||
ConnectorTokenClientContract,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { ExternalService, ExternalServiceCredentials, SNProductsConfigValue } from './types';
|
||||
|
||||
|
@ -21,6 +24,7 @@ interface CreateServiceWrapperOpts<T = ExternalService> {
|
|||
serviceConfig: SNProductsConfigValue;
|
||||
connectorTokenClient: ConnectorTokenClientContract;
|
||||
createServiceFn: ServiceFactory<T>;
|
||||
connectorUsageCollector: ConnectorUsageCollector;
|
||||
}
|
||||
|
||||
export function createServiceWrapper<T = ExternalService>({
|
||||
|
@ -31,6 +35,7 @@ export function createServiceWrapper<T = ExternalService>({
|
|||
serviceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn,
|
||||
connectorUsageCollector,
|
||||
}: CreateServiceWrapperOpts<T>): T {
|
||||
const { config } = credentials;
|
||||
const { apiUrl: url } = config as ServiceNowPublicConfigurationType;
|
||||
|
@ -50,5 +55,6 @@ export function createServiceWrapper<T = ExternalService>({
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks';
|
|||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { serviceNowCommonFields, serviceNowChoices } from './mocks';
|
||||
import { snExternalServiceConfig } from './config';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios', () => ({
|
||||
|
@ -178,6 +179,7 @@ const expectImportedIncident = (update: boolean) => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -191,6 +193,7 @@ const expectImportedIncident = (update: boolean) => {
|
|||
u_description: 'desc',
|
||||
...(update ? { elastic_incident_id: '1' } : {}),
|
||||
},
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -199,14 +202,21 @@ const expectImportedIncident = (update: boolean) => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
};
|
||||
|
||||
describe('ServiceNow service', () => {
|
||||
let service: ExternalService;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
// The trailing slash at the end of the url is intended.
|
||||
|
@ -218,6 +228,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -233,6 +244,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -273,6 +285,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -437,6 +450,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -464,6 +478,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -477,6 +492,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
@ -490,6 +506,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -535,6 +552,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident?sysparm_query=ORDERBYDESCsys_created_on^correlation_id=custom_correlation_id',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -559,6 +577,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
@ -572,6 +591,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident?sysparm_query=ORDERBYDESCsys_created_on^correlation_id=custom_correlation_id',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -625,6 +645,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const res = await createIncident(service);
|
||||
|
@ -635,6 +656,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -644,6 +666,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
|
||||
method: 'post',
|
||||
data: { u_short_description: 'title', u_description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -652,6 +675,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -707,6 +731,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident',
|
||||
method: 'post',
|
||||
data: { u_short_description: 'title', u_description: 'desc', foo: 'test' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -723,6 +748,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -749,6 +775,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/incident',
|
||||
method: 'post',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -762,6 +789,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
|
@ -778,6 +806,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/sn_si_incident',
|
||||
method: 'post',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -826,6 +855,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const res = await updateIncident(service);
|
||||
|
@ -835,6 +865,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -844,6 +875,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
|
||||
method: 'post',
|
||||
data: { u_short_description: 'title', u_description: 'desc', elastic_incident_id: '1' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -852,6 +884,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -915,6 +948,7 @@ describe('ServiceNow service', () => {
|
|||
elastic_incident_id: '1',
|
||||
foo: 'test',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -931,6 +965,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -958,6 +993,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'patch',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -971,6 +1007,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
|
@ -988,6 +1025,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'patch',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -1032,6 +1070,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -1040,6 +1079,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -1054,6 +1094,7 @@ describe('ServiceNow service', () => {
|
|||
u_state: '7',
|
||||
u_close_notes: 'Closed by Caller',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(4, {
|
||||
|
@ -1062,6 +1103,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res?.url).toEqual('https://example.com/nav_to.do?uri=incident.do?sys_id=1');
|
||||
|
@ -1097,6 +1139,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident?sysparm_query=ORDERBYDESCsys_created_on^correlation_id=custom_correlation_id',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -1105,6 +1148,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -1119,6 +1163,7 @@ describe('ServiceNow service', () => {
|
|||
u_state: '7',
|
||||
u_close_notes: 'Closed by Caller',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(4, {
|
||||
|
@ -1127,6 +1172,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res?.url).toEqual('https://example.com/nav_to.do?uri=incident.do?sys_id=1');
|
||||
|
@ -1237,6 +1283,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1268,6 +1315,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
|
@ -1285,6 +1333,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -1298,6 +1347,7 @@ describe('ServiceNow service', () => {
|
|||
state: '7',
|
||||
close_notes: 'Closed by Caller',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -1306,6 +1356,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res?.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -1325,6 +1376,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1346,6 +1398,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
@ -1358,6 +1411,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1394,6 +1448,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category^language=en&sysparm_fields=label,value,dependent_value,element',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1415,6 +1470,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
@ -1428,6 +1484,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category^language=en&sysparm_fields=label,value,dependent_value,element',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1520,6 +1577,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
await service.checkIfApplicationIsInstalled();
|
||||
expect(requestMock).not.toHaveBeenCalled();
|
||||
|
|
|
@ -37,6 +37,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
connectorUsageCollector,
|
||||
}): ExternalService => {
|
||||
const { config, secrets } = credentials;
|
||||
const { table, importSetTable, useImportAPI, appScope } = serviceConfig;
|
||||
|
@ -132,6 +133,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
logger,
|
||||
configurationUtilities,
|
||||
method: 'get',
|
||||
connectorUsageCollector, // TODO check if this is internal
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
@ -160,6 +162,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
logger,
|
||||
configurationUtilities,
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
@ -178,6 +181,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
logger,
|
||||
params,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
@ -201,6 +205,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
method: 'post',
|
||||
data: prepareIncident(useTableApi, incident),
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
@ -240,6 +245,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
...(useTableApi ? {} : { elastic_incident_id: incidentId }),
|
||||
},
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
@ -272,6 +278,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
method: 'get',
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
@ -350,6 +357,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
url: fieldsUrl,
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
checkInstance(res);
|
||||
|
@ -367,6 +375,7 @@ export const createExternalService: ServiceFactory = ({
|
|||
url: getChoicesURL(fields),
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
checkInstance(res);
|
||||
return res.data.result;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
|||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { ValidatorServices } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, ValidatorServices } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
ExecutorParamsSchemaITSM,
|
||||
ExecutorSubActionCommonFieldsParamsSchema,
|
||||
|
@ -305,6 +305,7 @@ interface ServiceFactoryOpts {
|
|||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
serviceConfig: SNProductsConfigValue;
|
||||
axiosInstance: AxiosInstance;
|
||||
connectorUsageCollector: ConnectorUsageCollector;
|
||||
}
|
||||
|
||||
export type ServiceFactory<T = ExternalService> = ({
|
||||
|
@ -313,6 +314,7 @@ export type ServiceFactory<T = ExternalService> = ({
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
connectorUsageCollector,
|
||||
}: ServiceFactoryOpts) => T;
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,6 +19,7 @@ import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
|||
import { RunActionResponseSchema, StreamingResponseSchema } from '../../../common/openai/schema';
|
||||
import { initDashboard } from '../lib/gen_ai/create_gen_ai_dashboard';
|
||||
import { PassThrough, Transform } from 'stream';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
jest.mock('../lib/gen_ai/create_gen_ai_dashboard');
|
||||
const mockTee = jest.fn();
|
||||
|
||||
|
@ -46,6 +47,9 @@ jest.mock('openai', () => ({
|
|||
describe('OpenAIConnector', () => {
|
||||
let mockRequest: jest.Mock;
|
||||
let mockError: jest.Mock;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const mockResponseString = 'Hello! How can I assist you today?';
|
||||
const mockResponse = {
|
||||
headers: {},
|
||||
|
@ -72,6 +76,10 @@ describe('OpenAIConnector', () => {
|
|||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
||||
mockError = jest.fn().mockImplementation(() => {
|
||||
throw new Error('API Error');
|
||||
|
@ -92,7 +100,7 @@ describe('OpenAIConnector', () => {
|
|||
},
|
||||
},
|
||||
secrets: { apiKey: '123' },
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
|
||||
|
@ -113,48 +121,74 @@ describe('OpenAIConnector', () => {
|
|||
|
||||
describe('runApi', () => {
|
||||
it('uses the default model if none is supplied', async () => {
|
||||
const response = await connector.runApi({ body: JSON.stringify(sampleOpenAiBody) });
|
||||
const response = await connector.runApi(
|
||||
{ body: JSON.stringify(sampleOpenAiBody) },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: false, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: false,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('overrides the default model with the default model specified in the body', async () => {
|
||||
const requestBody = { model: 'gpt-3.5-turbo', ...sampleOpenAiBody };
|
||||
const response = await connector.runApi({ body: JSON.stringify(requestBody) });
|
||||
const response = await connector.runApi(
|
||||
{ body: JSON.stringify(requestBody) },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...requestBody, stream: false }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...requestBody, stream: false }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('the OpenAI API call is successful with correct parameters', async () => {
|
||||
const response = await connector.runApi({ body: JSON.stringify(sampleOpenAiBody) });
|
||||
const response = await connector.runApi(
|
||||
{ body: JSON.stringify(sampleOpenAiBody) },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: false, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: false,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
|
@ -168,25 +202,31 @@ describe('OpenAIConnector', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
const response = await connector.runApi({
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...body,
|
||||
stream: false,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
const response = await connector.runApi(
|
||||
{
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
stream: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...body,
|
||||
stream: false,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
|
@ -194,51 +234,71 @@ describe('OpenAIConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.runApi({ body: JSON.stringify(sampleOpenAiBody) })).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
await expect(
|
||||
connector.runApi({ body: JSON.stringify(sampleOpenAiBody) }, connectorUsageCollector)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('streamApi', () => {
|
||||
it('the OpenAI API call is successful with correct parameters when stream = false', async () => {
|
||||
const response = await connector.streamApi({
|
||||
body: JSON.stringify(sampleOpenAiBody),
|
||||
stream: false,
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: false, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
const response = await connector.streamApi(
|
||||
{
|
||||
body: JSON.stringify(sampleOpenAiBody),
|
||||
stream: false,
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: false,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('the OpenAI API call is successful with correct parameters when stream = true', async () => {
|
||||
const response = await connector.streamApi({
|
||||
body: JSON.stringify(sampleOpenAiBody),
|
||||
stream: true,
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
responseType: 'stream',
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: true, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
const response = await connector.streamApi(
|
||||
{
|
||||
body: JSON.stringify(sampleOpenAiBody),
|
||||
stream: true,
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
responseType: 'stream',
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: true,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({
|
||||
headers: { 'Content-Type': 'dont-compress-this' },
|
||||
...mockResponse.data,
|
||||
|
@ -255,29 +315,35 @@ describe('OpenAIConnector', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
const response = await connector.streamApi({
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
stream: false,
|
||||
}),
|
||||
stream: true,
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
responseType: 'stream',
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...body,
|
||||
const response = await connector.streamApi(
|
||||
{
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
stream: false,
|
||||
}),
|
||||
stream: true,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
responseType: 'stream',
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...body,
|
||||
stream: true,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({
|
||||
headers: { 'Content-Type': 'dont-compress-this' },
|
||||
...mockResponse.data,
|
||||
|
@ -289,7 +355,10 @@ describe('OpenAIConnector', () => {
|
|||
connector.request = mockError;
|
||||
|
||||
await expect(
|
||||
connector.streamApi({ body: JSON.stringify(sampleOpenAiBody), stream: true })
|
||||
connector.streamApi(
|
||||
{ body: JSON.stringify(sampleOpenAiBody), stream: true },
|
||||
connectorUsageCollector
|
||||
)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
@ -314,135 +383,181 @@ describe('OpenAIConnector', () => {
|
|||
});
|
||||
|
||||
it('the API call is successful with correct request parameters', async () => {
|
||||
await connector.invokeStream(sampleOpenAiBody);
|
||||
await connector.invokeStream(sampleOpenAiBody, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: true, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: true,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('signal is properly passed to streamApi', async () => {
|
||||
const signal = jest.fn();
|
||||
await connector.invokeStream({ ...sampleOpenAiBody, signal });
|
||||
await connector.invokeStream({ ...sampleOpenAiBody, signal }, connectorUsageCollector);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: true, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: true,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
signal,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('timeout is properly passed to streamApi', async () => {
|
||||
const timeout = 180000;
|
||||
await connector.invokeStream({ ...sampleOpenAiBody, timeout });
|
||||
await connector.invokeStream({ ...sampleOpenAiBody, timeout }, connectorUsageCollector);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: true, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://api.openai.com/v1/chat/completions',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
responseType: 'stream',
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: true,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
timeout,
|
||||
},
|
||||
timeout,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('errors during API calls are properly handled', async () => {
|
||||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.invokeStream(sampleOpenAiBody)).rejects.toThrow('API Error');
|
||||
await expect(
|
||||
connector.invokeStream(sampleOpenAiBody, connectorUsageCollector)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
|
||||
it('responds with a readable stream', async () => {
|
||||
// @ts-ignore
|
||||
connector.request = mockStream();
|
||||
const response = await connector.invokeStream(sampleOpenAiBody);
|
||||
const response = await connector.invokeStream(sampleOpenAiBody, connectorUsageCollector);
|
||||
expect(response instanceof PassThrough).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invokeAI', () => {
|
||||
it('the API call is successful with correct parameters', async () => {
|
||||
const response = await connector.invokeAI(sampleOpenAiBody);
|
||||
const response = await connector.invokeAI(sampleOpenAiBody, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: false, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: false,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response.message).toEqual(mockResponseString);
|
||||
expect(response.usage.total_tokens).toEqual(9);
|
||||
});
|
||||
|
||||
it('signal is properly passed to runApi', async () => {
|
||||
const signal = jest.fn();
|
||||
await connector.invokeAI({ ...sampleOpenAiBody, signal });
|
||||
await connector.invokeAI({ ...sampleOpenAiBody, signal }, connectorUsageCollector);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: false, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: false,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
signal,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('timeout is properly passed to runApi', async () => {
|
||||
const timeout = 180000;
|
||||
await connector.invokeAI({ ...sampleOpenAiBody, timeout });
|
||||
await connector.invokeAI({ ...sampleOpenAiBody, timeout }, connectorUsageCollector);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: false, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: false,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'X-My-Custom-Header': 'foo',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
timeout,
|
||||
},
|
||||
timeout,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('errors during API calls are properly handled', async () => {
|
||||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.invokeAI(sampleOpenAiBody)).rejects.toThrow('API Error');
|
||||
await expect(connector.invokeAI(sampleOpenAiBody, connectorUsageCollector)).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invokeAsyncIterator', () => {
|
||||
it('the API call is successful with correct request parameters', async () => {
|
||||
await connector.invokeAsyncIterator(sampleOpenAiBody);
|
||||
await connector.invokeAsyncIterator(sampleOpenAiBody, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(0);
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -457,7 +572,10 @@ describe('OpenAIConnector', () => {
|
|||
it('signal and timeout is properly passed', async () => {
|
||||
const timeout = 180000;
|
||||
const signal = jest.fn();
|
||||
await connector.invokeAsyncIterator({ ...sampleOpenAiBody, signal, timeout });
|
||||
await connector.invokeAsyncIterator(
|
||||
{ ...sampleOpenAiBody, signal, timeout },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(0);
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
{
|
||||
|
@ -478,7 +596,9 @@ describe('OpenAIConnector', () => {
|
|||
throw new Error('API Error');
|
||||
});
|
||||
|
||||
await expect(connector.invokeAsyncIterator(sampleOpenAiBody)).rejects.toThrow('API Error');
|
||||
await expect(
|
||||
connector.invokeAsyncIterator(sampleOpenAiBody, connectorUsageCollector)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
describe('getResponseErrorMessage', () => {
|
||||
|
@ -568,16 +688,26 @@ describe('OpenAIConnector', () => {
|
|||
|
||||
describe('runApi', () => {
|
||||
it('uses the default model if none is supplied', async () => {
|
||||
const response = await connector.runApi({ body: JSON.stringify(sampleOpenAiBody) });
|
||||
const response = await connector.runApi(
|
||||
{ body: JSON.stringify(sampleOpenAiBody) },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({ ...sampleOpenAiBody, stream: false, model: DEFAULT_OPENAI_MODEL }),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
data: JSON.stringify({
|
||||
...sampleOpenAiBody,
|
||||
stream: false,
|
||||
model: DEFAULT_OPENAI_MODEL,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer 123',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
});
|
||||
|
@ -614,17 +744,23 @@ describe('OpenAIConnector', () => {
|
|||
|
||||
describe('runApi', () => {
|
||||
it('test the AzureAI API call is successful with correct parameters', async () => {
|
||||
const response = await connector.runApi({ body: JSON.stringify(sampleAzureAiBody) });
|
||||
const response = await connector.runApi(
|
||||
{ body: JSON.stringify(sampleAzureAiBody) },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: false }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: false }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
|
@ -637,19 +773,25 @@ describe('OpenAIConnector', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
const response = await connector.runApi({
|
||||
body: JSON.stringify({ ...body, stream: true }),
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
...mockDefaults,
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: false }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
const response = await connector.runApi(
|
||||
{
|
||||
body: JSON.stringify({ ...body, stream: true }),
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
...mockDefaults,
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: false }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
|
@ -657,49 +799,61 @@ describe('OpenAIConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.runApi({ body: JSON.stringify(sampleAzureAiBody) })).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
await expect(
|
||||
connector.runApi({ body: JSON.stringify(sampleAzureAiBody) }, connectorUsageCollector)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('streamApi', () => {
|
||||
it('the AzureAI API call is successful with correct parameters when stream = false', async () => {
|
||||
const response = await connector.streamApi({
|
||||
body: JSON.stringify(sampleAzureAiBody),
|
||||
stream: false,
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: false }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
const response = await connector.streamApi(
|
||||
{
|
||||
body: JSON.stringify(sampleAzureAiBody),
|
||||
stream: false,
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: false }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('the AzureAI API call is successful with correct parameters when stream = true', async () => {
|
||||
const response = await connector.streamApi({
|
||||
body: JSON.stringify(sampleAzureAiBody),
|
||||
stream: true,
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
responseType: 'stream',
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: true }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
const response = await connector.streamApi(
|
||||
{
|
||||
body: JSON.stringify(sampleAzureAiBody),
|
||||
stream: true,
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
responseType: 'stream',
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({ ...sampleAzureAiBody, stream: true }),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({
|
||||
headers: { 'Content-Type': 'dont-compress-this' },
|
||||
...mockResponse.data,
|
||||
|
@ -715,25 +869,31 @@ describe('OpenAIConnector', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
const response = await connector.streamApi({
|
||||
body: JSON.stringify({ ...body, stream: false }),
|
||||
stream: true,
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
responseType: 'stream',
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...body,
|
||||
const response = await connector.streamApi(
|
||||
{
|
||||
body: JSON.stringify({ ...body, stream: false }),
|
||||
stream: true,
|
||||
}),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
responseType: 'stream',
|
||||
url: 'https://My-test-resource-123.openai.azure.com/openai/deployments/NEW-DEPLOYMENT-321/chat/completions?api-version=2023-05-15',
|
||||
method: 'post',
|
||||
responseSchema: StreamingResponseSchema,
|
||||
data: JSON.stringify({
|
||||
...body,
|
||||
stream: true,
|
||||
}),
|
||||
headers: {
|
||||
'api-key': '123',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({
|
||||
headers: { 'Content-Type': 'dont-compress-this' },
|
||||
...mockResponse.data,
|
||||
|
@ -745,7 +905,10 @@ describe('OpenAIConnector', () => {
|
|||
connector.request = mockError;
|
||||
|
||||
await expect(
|
||||
connector.streamApi({ body: JSON.stringify(sampleAzureAiBody), stream: true })
|
||||
connector.streamApi(
|
||||
{ body: JSON.stringify(sampleAzureAiBody), stream: true },
|
||||
connectorUsageCollector
|
||||
)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
ChatCompletionMessageParam,
|
||||
} from 'openai/resources/chat/completions';
|
||||
import { Stream } from 'openai/streaming';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { removeEndpointFromUrl } from './lib/openai_utils';
|
||||
import {
|
||||
RunActionParamsSchema,
|
||||
|
@ -156,7 +157,11 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
* responsible for making a POST request to the external API endpoint and returning the response data
|
||||
* @param body The stringified request body to be sent in the POST request.
|
||||
*/
|
||||
public async runApi({ body, signal, timeout }: RunActionParams): Promise<RunActionResponse> {
|
||||
|
||||
public async runApi(
|
||||
{ body, signal, timeout }: RunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<RunActionResponse> {
|
||||
const sanitizedBody = sanitizeRequest(
|
||||
this.provider,
|
||||
this.url,
|
||||
|
@ -164,20 +169,23 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
...('defaultModel' in this.config ? [this.config.defaultModel] : [])
|
||||
);
|
||||
const axiosOptions = getAxiosOptions(this.provider, this.key, false);
|
||||
const response = await this.request({
|
||||
url: this.url,
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: sanitizedBody,
|
||||
signal,
|
||||
// give up to 2 minutes for response
|
||||
timeout: timeout ?? DEFAULT_TIMEOUT_MS,
|
||||
...axiosOptions,
|
||||
headers: {
|
||||
...this.config.headers,
|
||||
...axiosOptions.headers,
|
||||
const response = await this.request(
|
||||
{
|
||||
url: this.url,
|
||||
method: 'post',
|
||||
responseSchema: RunActionResponseSchema,
|
||||
data: sanitizedBody,
|
||||
signal,
|
||||
// give up to 2 minutes for response
|
||||
timeout: timeout ?? DEFAULT_TIMEOUT_MS,
|
||||
...axiosOptions,
|
||||
headers: {
|
||||
...this.config.headers,
|
||||
...axiosOptions.headers,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
@ -189,12 +197,10 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
* @param body request body for the API request
|
||||
* @param stream flag indicating whether it is a streaming request or not
|
||||
*/
|
||||
public async streamApi({
|
||||
body,
|
||||
stream,
|
||||
signal,
|
||||
timeout,
|
||||
}: StreamActionParams): Promise<RunActionResponse> {
|
||||
public async streamApi(
|
||||
{ body, stream, signal, timeout }: StreamActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<RunActionResponse> {
|
||||
const executeBody = getRequestWithStreamOption(
|
||||
this.provider,
|
||||
this.url,
|
||||
|
@ -205,19 +211,22 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
|
||||
const axiosOptions = getAxiosOptions(this.provider, this.key, stream);
|
||||
|
||||
const response = await this.request({
|
||||
url: this.url,
|
||||
method: 'post',
|
||||
responseSchema: stream ? StreamingResponseSchema : RunActionResponseSchema,
|
||||
data: executeBody,
|
||||
signal,
|
||||
...axiosOptions,
|
||||
headers: {
|
||||
...this.config.headers,
|
||||
...axiosOptions.headers,
|
||||
const response = await this.request(
|
||||
{
|
||||
url: this.url,
|
||||
method: 'post',
|
||||
responseSchema: stream ? StreamingResponseSchema : RunActionResponseSchema,
|
||||
data: executeBody,
|
||||
signal,
|
||||
...axiosOptions,
|
||||
headers: {
|
||||
...this.config.headers,
|
||||
...axiosOptions.headers,
|
||||
},
|
||||
timeout,
|
||||
},
|
||||
timeout,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
return stream ? pipeStreamingResponse(response) : response.data;
|
||||
}
|
||||
|
||||
|
@ -264,15 +273,21 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
* returned directly to the client for streaming
|
||||
* @param body - the OpenAI Invoke request body
|
||||
*/
|
||||
public async invokeStream(body: InvokeAIActionParams): Promise<PassThrough> {
|
||||
public async invokeStream(
|
||||
body: InvokeAIActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<PassThrough> {
|
||||
const { signal, timeout, ...rest } = body;
|
||||
|
||||
const res = (await this.streamApi({
|
||||
body: JSON.stringify(rest),
|
||||
stream: true,
|
||||
signal,
|
||||
timeout, // do not default if not provided
|
||||
})) as unknown as IncomingMessage;
|
||||
const res = (await this.streamApi(
|
||||
{
|
||||
body: JSON.stringify(rest),
|
||||
stream: true,
|
||||
signal,
|
||||
timeout, // do not default if not provided
|
||||
},
|
||||
connectorUsageCollector
|
||||
)) as unknown as IncomingMessage;
|
||||
|
||||
return res.pipe(new PassThrough());
|
||||
}
|
||||
|
@ -286,7 +301,10 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
* tokenCountStream: Stream<ChatCompletionChunk>; the result for token counting stream
|
||||
* }
|
||||
*/
|
||||
public async invokeAsyncIterator(body: InvokeAIActionParams): Promise<{
|
||||
public async invokeAsyncIterator(
|
||||
body: InvokeAIActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<{
|
||||
consumerStream: Stream<ChatCompletionChunk>;
|
||||
tokenCountStream: Stream<ChatCompletionChunk>;
|
||||
}> {
|
||||
|
@ -301,6 +319,8 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
rest.model ??
|
||||
('defaultModel' in this.config ? this.config.defaultModel : DEFAULT_OPENAI_MODEL),
|
||||
};
|
||||
|
||||
connectorUsageCollector.addRequestBodyBytes(undefined, requestBody);
|
||||
const stream = await this.openAI.chat.completions.create(requestBody, {
|
||||
signal,
|
||||
timeout, // do not default if not provided
|
||||
|
@ -323,9 +343,15 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
|
|||
* @param body - the OpenAI chat completion request body
|
||||
* @returns an object with the response string and the usage object
|
||||
*/
|
||||
public async invokeAI(body: InvokeAIActionParams): Promise<InvokeAIActionResponse> {
|
||||
public async invokeAI(
|
||||
body: InvokeAIActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<InvokeAIActionResponse> {
|
||||
const { signal, timeout, ...rest } = body;
|
||||
const res = await this.runApi({ body: JSON.stringify(rest), signal, timeout });
|
||||
const res = await this.runApi(
|
||||
{ body: JSON.stringify(rest), signal, timeout },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
if (res.choices && res.choices.length > 0 && res.choices[0].message?.content) {
|
||||
const result = res.choices[0].message.content.trim();
|
||||
|
|
|
@ -15,6 +15,7 @@ import { MockedLogger } from '@kbn/logging-mocks';
|
|||
import { OpsgenieConnectorTypeId } from '../../../common';
|
||||
import { OpsgenieConnector } from './connector';
|
||||
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
|
@ -36,6 +37,7 @@ describe('OpsgenieConnector', () => {
|
|||
let mockedActionsConfig: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let logger: MockedLogger;
|
||||
let services: ReturnType<typeof actionsMock.createServices>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
const defaultCreateAlertExpect = {
|
||||
method: 'post',
|
||||
|
@ -75,36 +77,43 @@ describe('OpsgenieConnector', () => {
|
|||
logger,
|
||||
services,
|
||||
});
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls request with the correct arguments for creating an alert', async () => {
|
||||
await connector.createAlert({ message: 'hello' });
|
||||
await connector.createAlert({ message: 'hello' }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock.mock.calls[0][0]).toEqual({
|
||||
data: { message: 'hello' },
|
||||
...ignoredRequestFields,
|
||||
...defaultCreateAlertExpect,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls request without modifying the alias when it is less than 512 characters when creating an alert', async () => {
|
||||
await connector.createAlert({ message: 'hello', alias: '111' });
|
||||
await connector.createAlert({ message: 'hello', alias: '111' }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock.mock.calls[0][0]).toEqual({
|
||||
...ignoredRequestFields,
|
||||
...defaultCreateAlertExpect,
|
||||
data: { message: 'hello', alias: '111' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls request without modifying the alias when it is equal to 512 characters when creating an alert', async () => {
|
||||
const alias = 'a'.repeat(512);
|
||||
await connector.createAlert({ message: 'hello', alias });
|
||||
await connector.createAlert({ message: 'hello', alias }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock.mock.calls[0][0]).toEqual({
|
||||
...ignoredRequestFields,
|
||||
...defaultCreateAlertExpect,
|
||||
data: { message: 'hello', alias },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -114,12 +123,13 @@ describe('OpsgenieConnector', () => {
|
|||
const hasher = crypto.createHash('sha256');
|
||||
const sha256Hash = hasher.update(alias);
|
||||
|
||||
await connector.createAlert({ message: 'hello', alias });
|
||||
await connector.createAlert({ message: 'hello', alias }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock.mock.calls[0][0]).toEqual({
|
||||
...ignoredRequestFields,
|
||||
...defaultCreateAlertExpect,
|
||||
data: { message: 'hello', alias: `sha-${sha256Hash.digest('hex')}` },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -129,22 +139,24 @@ describe('OpsgenieConnector', () => {
|
|||
const hasher = crypto.createHash('sha256');
|
||||
const sha256Hash = hasher.update(alias);
|
||||
|
||||
await connector.closeAlert({ alias });
|
||||
await connector.closeAlert({ alias }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock.mock.calls[0][0]).toEqual({
|
||||
...ignoredRequestFields,
|
||||
...createCloseAlertExpect(`sha-${sha256Hash.digest('hex')}`),
|
||||
data: {},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls request with the correct arguments for closing an alert', async () => {
|
||||
await connector.closeAlert({ user: 'sam', alias: '111' });
|
||||
await connector.closeAlert({ user: 'sam', alias: '111' }, connectorUsageCollector);
|
||||
|
||||
expect(requestMock.mock.calls[0][0]).toEqual({
|
||||
...ignoredRequestFields,
|
||||
...createCloseAlertExpect('111'),
|
||||
data: { user: 'sam' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import crypto from 'crypto';
|
|||
import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { OpsgenieSubActions } from '../../../common';
|
||||
import { CreateAlertParamsSchema, CloseAlertParamsSchema, Response } from './schema';
|
||||
import { CloseAlertParams, Config, CreateAlertParams, FailureResponseType, Secrets } from './types';
|
||||
|
@ -67,14 +68,20 @@ export class OpsgenieConnector extends SubActionConnector<Config, Secrets> {
|
|||
}
|
||||
}
|
||||
|
||||
public async createAlert(params: CreateAlertParams) {
|
||||
const res = await this.request({
|
||||
method: 'post',
|
||||
url: this.concatPathToURL('v2/alerts').toString(),
|
||||
data: { ...params, ...OpsgenieConnector.createAliasObj(params.alias) },
|
||||
headers: this.createHeaders(),
|
||||
responseSchema: Response,
|
||||
});
|
||||
public async createAlert(
|
||||
params: CreateAlertParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'post',
|
||||
url: this.concatPathToURL('v2/alerts').toString(),
|
||||
data: { ...params, ...OpsgenieConnector.createAliasObj(params.alias) },
|
||||
headers: this.createHeaders(),
|
||||
responseSchema: Response,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
@ -107,7 +114,10 @@ export class OpsgenieConnector extends SubActionConnector<Config, Secrets> {
|
|||
return { Authorization: `GenieKey ${this.secrets.apiKey}` };
|
||||
}
|
||||
|
||||
public async closeAlert(params: CloseAlertParams) {
|
||||
public async closeAlert(
|
||||
params: CloseAlertParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const newAlias = OpsgenieConnector.createAlias(params.alias);
|
||||
|
||||
const fullURL = this.concatPathToURL(`v2/alerts/${newAlias}/close`);
|
||||
|
@ -115,13 +125,16 @@ export class OpsgenieConnector extends SubActionConnector<Config, Secrets> {
|
|||
|
||||
const { alias, ...paramsWithoutAlias } = params;
|
||||
|
||||
const res = await this.request({
|
||||
method: 'post',
|
||||
url: fullURL.toString(),
|
||||
data: paramsWithoutAlias,
|
||||
headers: this.createHeaders(),
|
||||
responseSchema: Response,
|
||||
});
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'post',
|
||||
url: fullURL.toString(),
|
||||
data: paramsWithoutAlias,
|
||||
headers: this.createHeaders(),
|
||||
responseSchema: Response,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import moment from 'moment';
|
|||
jest.mock('./post_pagerduty', () => ({
|
||||
postPagerduty: jest.fn(),
|
||||
}));
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, Services } from '@kbn/actions-plugin/server/types';
|
||||
import { validateConfig, validateSecrets, validateParams } from '@kbn/actions-plugin/server/lib';
|
||||
import { postPagerduty } from './post_pagerduty';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
|
@ -31,10 +31,15 @@ const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
|||
|
||||
let connectorType: PagerDutyConnectorType;
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
|
@ -269,6 +274,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
@ -350,6 +356,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
@ -458,6 +465,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
@ -535,6 +543,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
@ -578,6 +587,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
expect(actionResponse).toMatchInlineSnapshot(`
|
||||
|
@ -608,6 +618,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
expect(actionResponse).toMatchInlineSnapshot(`
|
||||
|
@ -638,6 +649,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
expect(actionResponse).toMatchInlineSnapshot(`
|
||||
|
@ -668,6 +680,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
expect(actionResponse).toMatchInlineSnapshot(`
|
||||
|
@ -708,6 +721,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
@ -771,6 +785,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
@ -837,6 +852,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
@ -902,6 +918,7 @@ describe('execute()', () => {
|
|||
services,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
};
|
||||
const actionResponse = await connectorType.executor(executorOptions);
|
||||
const { apiUrl, data, headers } = postPagerdutyMock.mock.calls[0][0];
|
||||
|
|
|
@ -198,8 +198,16 @@ function getPagerDutyApiUrl(config: ConnectorTypeConfigType): string {
|
|||
async function executor(
|
||||
execOptions: PagerDutyConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> {
|
||||
const { actionId, config, secrets, params, services, configurationUtilities, logger } =
|
||||
execOptions;
|
||||
const {
|
||||
actionId,
|
||||
config,
|
||||
secrets,
|
||||
params,
|
||||
services,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
} = execOptions;
|
||||
|
||||
const apiUrl = getPagerDutyApiUrl(config);
|
||||
const headers = {
|
||||
|
@ -213,7 +221,8 @@ async function executor(
|
|||
response = await postPagerduty(
|
||||
{ apiUrl, data, headers, services },
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
} catch (err) {
|
||||
const message = i18n.translate('xpack.stackConnectors.pagerduty.postingErrorMessage', {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, Services } from '@kbn/actions-plugin/server/types';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
|
||||
|
@ -22,7 +22,8 @@ interface PostPagerdutyOptions {
|
|||
export async function postPagerduty(
|
||||
options: PostPagerdutyOptions,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<AxiosResponse> {
|
||||
const { apiUrl, data, headers } = options;
|
||||
const axiosInstance = axios.create();
|
||||
|
@ -36,5 +37,6 @@ export async function postPagerduty(
|
|||
headers,
|
||||
configurationUtilities,
|
||||
validateStatus: () => true,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ResilientConnector } from './resilient';
|
|||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { RESILIENT_CONNECTOR_ID } from './constants';
|
||||
import { PushToServiceIncidentSchema } from './schema';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
|
@ -83,13 +84,15 @@ const mockIncidentUpdate = (withUpdateError = false) => {
|
|||
})
|
||||
);
|
||||
};
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
describe('IBM Resilient connector', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const connector = new ResilientConnector(
|
||||
{
|
||||
connector: { id: '1', type: RESILIENT_CONNECTOR_ID },
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
config: { orgId, apiUrl },
|
||||
secrets: { apiKeyId, apiKeySecret },
|
||||
|
@ -107,6 +110,10 @@ describe('IBM Resilient connector', () => {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.setSystemTime(TIMESTAMP);
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIncident', () => {
|
||||
|
@ -129,12 +136,12 @@ describe('IBM Resilient connector', () => {
|
|||
});
|
||||
|
||||
it('returns the incident correctly', async () => {
|
||||
const res = await connector.getIncident({ id: '1' });
|
||||
const res = await connector.getIncident({ id: '1' }, connectorUsageCollector);
|
||||
expect(res).toEqual(incidentMock);
|
||||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getIncident({ id: '1' });
|
||||
await connector.getIncident({ id: '1' }, connectorUsageCollector);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
method: 'GET',
|
||||
|
@ -147,6 +154,7 @@ describe('IBM Resilient connector', () => {
|
|||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -154,7 +162,7 @@ describe('IBM Resilient connector', () => {
|
|||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(connector.getIncident({ id: '1' })).rejects.toThrow(
|
||||
await expect(connector.getIncident({ id: '1' }, connectorUsageCollector)).rejects.toThrow(
|
||||
'Unable to get incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
@ -183,7 +191,7 @@ describe('IBM Resilient connector', () => {
|
|||
});
|
||||
|
||||
it('creates the incident correctly', async () => {
|
||||
const res = await connector.createIncident(incidentMock);
|
||||
const res = await connector.createIncident(incidentMock, connectorUsageCollector);
|
||||
|
||||
expect(res).toEqual({
|
||||
title: '1',
|
||||
|
@ -194,7 +202,7 @@ describe('IBM Resilient connector', () => {
|
|||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.createIncident(incidentMock);
|
||||
await connector.createIncident(incidentMock, connectorUsageCollector);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
|
@ -214,6 +222,7 @@ describe('IBM Resilient connector', () => {
|
|||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -223,12 +232,15 @@ describe('IBM Resilient connector', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
connector.createIncident({
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
})
|
||||
connector.createIncident(
|
||||
{
|
||||
name: 'title',
|
||||
description: 'desc',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
connectorUsageCollector
|
||||
)
|
||||
).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred'
|
||||
);
|
||||
|
@ -237,7 +249,7 @@ describe('IBM Resilient connector', () => {
|
|||
it('should throw if the required attributes are not received in response', async () => {
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
|
||||
|
||||
await expect(connector.createIncident(incidentMock)).rejects.toThrow(
|
||||
await expect(connector.createIncident(incidentMock, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create incident. Error: Response validation failed (Error: [id]: expected value of type [number] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
|
@ -255,7 +267,7 @@ describe('IBM Resilient connector', () => {
|
|||
};
|
||||
it('updates the incident correctly', async () => {
|
||||
mockIncidentUpdate();
|
||||
const res = await connector.updateIncident(req);
|
||||
const res = await connector.updateIncident(req, connectorUsageCollector);
|
||||
|
||||
expect(res).toEqual({
|
||||
title: '1',
|
||||
|
@ -268,15 +280,18 @@ describe('IBM Resilient connector', () => {
|
|||
it('should call request with correct arguments', async () => {
|
||||
mockIncidentUpdate();
|
||||
|
||||
await connector.updateIncident({
|
||||
incidentId: '1',
|
||||
incident: {
|
||||
name: 'title_updated',
|
||||
description: 'desc_updated',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 5,
|
||||
await connector.updateIncident(
|
||||
{
|
||||
incidentId: '1',
|
||||
incident: {
|
||||
name: 'title_updated',
|
||||
description: 'desc_updated',
|
||||
incidentTypes: [1001],
|
||||
severityCode: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(requestMock.mock.calls[1][0]).toEqual({
|
||||
...ignoredRequestFields,
|
||||
|
@ -332,13 +347,14 @@ describe('IBM Resilient connector', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('it should throw an error', async () => {
|
||||
mockIncidentUpdate(true);
|
||||
|
||||
await expect(connector.updateIncident(req)).rejects.toThrow(
|
||||
await expect(connector.updateIncident(req, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
@ -361,7 +377,7 @@ describe('IBM Resilient connector', () => {
|
|||
);
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { notRequired: 'test' } }));
|
||||
|
||||
await expect(connector.updateIncident(req)).rejects.toThrow(
|
||||
await expect(connector.updateIncident(req, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to update incident with id 1. Error: Response validation failed (Error: [success]: expected value of type [boolean] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
|
@ -388,7 +404,7 @@ describe('IBM Resilient connector', () => {
|
|||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.addComment(req);
|
||||
await connector.addComment(req, connectorUsageCollector);
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
|
@ -404,6 +420,7 @@ describe('IBM Resilient connector', () => {
|
|||
format: 'text',
|
||||
},
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -412,7 +429,7 @@ describe('IBM Resilient connector', () => {
|
|||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(connector.addComment(req)).rejects.toThrow(
|
||||
await expect(connector.addComment(req, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
|
@ -428,7 +445,7 @@ describe('IBM Resilient connector', () => {
|
|||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getIncidentTypes();
|
||||
await connector.getIncidentTypes(undefined, connectorUsageCollector);
|
||||
expect(requestMock).toBeCalledTimes(1);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
|
@ -439,11 +456,12 @@ describe('IBM Resilient connector', () => {
|
|||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns incident types correctly', async () => {
|
||||
const res = await connector.getIncidentTypes();
|
||||
const res = await connector.getIncidentTypes(undefined, connectorUsageCollector);
|
||||
|
||||
expect(res).toEqual([
|
||||
{ id: '17', name: 'Communication error (fax; email)' },
|
||||
|
@ -456,7 +474,7 @@ describe('IBM Resilient connector', () => {
|
|||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(connector.getIncidentTypes()).rejects.toThrow(
|
||||
await expect(connector.getIncidentTypes(undefined, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
|
@ -466,7 +484,7 @@ describe('IBM Resilient connector', () => {
|
|||
createAxiosResponse({ data: { id: '1001', name: 'Custom type' } })
|
||||
);
|
||||
|
||||
await expect(connector.getIncidentTypes()).rejects.toThrow(
|
||||
await expect(connector.getIncidentTypes(undefined, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get incident types. Error: Response validation failed (Error: [values]: expected value of type [array] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
|
@ -484,7 +502,7 @@ describe('IBM Resilient connector', () => {
|
|||
});
|
||||
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getSeverity();
|
||||
await connector.getSeverity(undefined, connectorUsageCollector);
|
||||
expect(requestMock).toBeCalledTimes(1);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
...ignoredRequestFields,
|
||||
|
@ -495,11 +513,12 @@ describe('IBM Resilient connector', () => {
|
|||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns severity correctly', async () => {
|
||||
const res = await connector.getSeverity();
|
||||
const res = await connector.getSeverity(undefined, connectorUsageCollector);
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
|
@ -522,7 +541,7 @@ describe('IBM Resilient connector', () => {
|
|||
throw new Error('An error has occurred');
|
||||
});
|
||||
|
||||
await expect(connector.getSeverity()).rejects.toThrow(
|
||||
await expect(connector.getSeverity(undefined, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get severity. Error: An error has occurred.'
|
||||
);
|
||||
});
|
||||
|
@ -532,7 +551,7 @@ describe('IBM Resilient connector', () => {
|
|||
createAxiosResponse({ data: { id: '10', name: 'Critical' } })
|
||||
);
|
||||
|
||||
await expect(connector.getSeverity()).rejects.toThrow(
|
||||
await expect(connector.getSeverity(undefined, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get severity. Error: Response validation failed (Error: [values]: expected value of type [array] but got [undefined]).'
|
||||
);
|
||||
});
|
||||
|
@ -547,7 +566,7 @@ describe('IBM Resilient connector', () => {
|
|||
);
|
||||
});
|
||||
it('should call request with correct arguments', async () => {
|
||||
await connector.getFields();
|
||||
await connector.getFields(undefined, connectorUsageCollector);
|
||||
|
||||
expect(requestMock).toBeCalledTimes(1);
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -559,11 +578,12 @@ describe('IBM Resilient connector', () => {
|
|||
Authorization: `Basic ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns common fields correctly', async () => {
|
||||
const res = await connector.getFields();
|
||||
const res = await connector.getFields(undefined, connectorUsageCollector);
|
||||
expect(res).toEqual(resilientFields);
|
||||
});
|
||||
|
||||
|
@ -571,7 +591,7 @@ describe('IBM Resilient connector', () => {
|
|||
requestMock.mockImplementation(() => {
|
||||
throw new Error('An error has occurred');
|
||||
});
|
||||
await expect(connector.getFields()).rejects.toThrow(
|
||||
await expect(connector.getFields(undefined, connectorUsageCollector)).rejects.toThrow(
|
||||
'Unable to get fields. Error: An error has occurred'
|
||||
);
|
||||
});
|
||||
|
@ -579,7 +599,7 @@ describe('IBM Resilient connector', () => {
|
|||
it('should throw if the required attributes are not received in response', async () => {
|
||||
requestMock.mockImplementation(() => createAxiosResponse({ data: { someField: 'test' } }));
|
||||
|
||||
await expect(connector.getFields()).rejects.toThrow(
|
||||
await expect(connector.getFields(undefined, connectorUsageCollector)).rejects.toThrow(
|
||||
'[Action][IBM Resilient]: Unable to get fields. Error: Response validation failed (Error: expected value of type [array] but got [Object]).'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { omitBy, isNil } from 'lodash/fp';
|
|||
import { CaseConnector, getBasicAuthHeader, ServiceParams } from '@kbn/actions-plugin/server';
|
||||
import { schema, Type } from '@kbn/config-schema';
|
||||
import { getErrorMessage } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
CreateIncidentData,
|
||||
ExternalServiceIncidentResponse,
|
||||
|
@ -117,7 +118,10 @@ export class ResilientConnector extends CaseConnector<
|
|||
return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}/${key}`;
|
||||
}
|
||||
|
||||
public async createIncident(incident: Incident): Promise<ExternalServiceIncidentResponse> {
|
||||
public async createIncident(
|
||||
incident: Incident,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<ExternalServiceIncidentResponse> {
|
||||
try {
|
||||
let data: CreateIncidentData = {
|
||||
name: incident.name,
|
||||
|
@ -150,19 +154,22 @@ export class ResilientConnector extends CaseConnector<
|
|||
};
|
||||
}
|
||||
|
||||
const res = await this.request({
|
||||
url: `${this.urls.incident}?text_content_output_format=objects_convert`,
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object(
|
||||
{
|
||||
id: schema.number(),
|
||||
create_date: schema.number(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
});
|
||||
const res = await this.request(
|
||||
{
|
||||
url: `${this.urls.incident}?text_content_output_format=objects_convert`,
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object(
|
||||
{
|
||||
id: schema.number(),
|
||||
create_date: schema.number(),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
const { id, create_date: createDate } = res.data;
|
||||
|
||||
|
@ -179,30 +186,33 @@ export class ResilientConnector extends CaseConnector<
|
|||
}
|
||||
}
|
||||
|
||||
public async updateIncident({
|
||||
incidentId,
|
||||
incident,
|
||||
}: UpdateIncidentParams): Promise<ExternalServiceIncidentResponse> {
|
||||
public async updateIncident(
|
||||
{ incidentId, incident }: UpdateIncidentParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<ExternalServiceIncidentResponse> {
|
||||
try {
|
||||
const latestIncident = await this.getIncident({ id: incidentId });
|
||||
const latestIncident = await this.getIncident({ id: incidentId }, connectorUsageCollector);
|
||||
|
||||
// Remove null or undefined values. Allowing null values sets the field in IBM Resilient to empty.
|
||||
const newIncident = omitBy(isNil, incident);
|
||||
const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident });
|
||||
|
||||
const res = await this.request({
|
||||
method: 'PATCH',
|
||||
url: `${this.urls.incident}/${incidentId}`,
|
||||
data,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object({ success: schema.boolean() }, { unknowns: 'allow' }),
|
||||
});
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'PATCH',
|
||||
url: `${this.urls.incident}/${incidentId}`,
|
||||
data,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object({ success: schema.boolean() }, { unknowns: 'allow' }),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
if (!res.data.success) {
|
||||
throw new Error('Error while updating incident');
|
||||
}
|
||||
|
||||
const updatedIncident = await this.getIncident({ id: incidentId });
|
||||
const updatedIncident = await this.getIncident({ id: incidentId }, connectorUsageCollector);
|
||||
|
||||
return {
|
||||
title: `${updatedIncident.id}`,
|
||||
|
@ -220,15 +230,21 @@ export class ResilientConnector extends CaseConnector<
|
|||
}
|
||||
}
|
||||
|
||||
public async addComment({ incidentId, comment }: { incidentId: string; comment: string }) {
|
||||
public async addComment(
|
||||
{ incidentId, comment }: { incidentId: string; comment: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
try {
|
||||
await this.request({
|
||||
method: 'POST',
|
||||
url: this.urls.comment.replace('{inc_id}', incidentId),
|
||||
data: { text: { format: 'text', content: comment } },
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object({}, { unknowns: 'allow' }),
|
||||
});
|
||||
await this.request(
|
||||
{
|
||||
method: 'POST',
|
||||
url: this.urls.comment.replace('{inc_id}', incidentId),
|
||||
data: { text: { format: 'text', content: comment } },
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: schema.object({}, { unknowns: 'allow' }),
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getErrorMessage(
|
||||
|
@ -239,17 +255,23 @@ export class ResilientConnector extends CaseConnector<
|
|||
}
|
||||
}
|
||||
|
||||
public async getIncident({ id }: { id: string }): Promise<GetIncidentResponse> {
|
||||
public async getIncident(
|
||||
{ id }: { id: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<GetIncidentResponse> {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: `${this.urls.incident}/${id}`,
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'GET',
|
||||
url: `${this.urls.incident}/${id}`,
|
||||
params: {
|
||||
text_content_output_format: 'objects_convert',
|
||||
},
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetIncidentResponseSchema,
|
||||
},
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetIncidentResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
|
@ -259,14 +281,20 @@ export class ResilientConnector extends CaseConnector<
|
|||
}
|
||||
}
|
||||
|
||||
public async getIncidentTypes(): Promise<GetIncidentTypesResponse> {
|
||||
public async getIncidentTypes(
|
||||
params: unknown,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<GetIncidentTypesResponse> {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: this.urls.incidentTypes,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetIncidentTypesResponseSchema,
|
||||
});
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'GET',
|
||||
url: this.urls.incidentTypes,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetIncidentTypesResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
const incidentTypes = res.data?.values ?? [];
|
||||
|
||||
|
@ -281,14 +309,20 @@ export class ResilientConnector extends CaseConnector<
|
|||
}
|
||||
}
|
||||
|
||||
public async getSeverity(): Promise<GetSeverityResponse> {
|
||||
public async getSeverity(
|
||||
params: unknown,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<GetSeverityResponse> {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: this.urls.severity,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetSeverityResponseSchema,
|
||||
});
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'GET',
|
||||
url: this.urls.severity,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetSeverityResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
const severities = res.data?.values ?? [];
|
||||
return severities.map((type: { value: number; label: string }) => ({
|
||||
|
@ -302,14 +336,17 @@ export class ResilientConnector extends CaseConnector<
|
|||
}
|
||||
}
|
||||
|
||||
public async getFields() {
|
||||
public async getFields(params: unknown, connectorUsageCollector: ConnectorUsageCollector) {
|
||||
try {
|
||||
const res = await this.request({
|
||||
method: 'GET',
|
||||
url: this.getIncidentFieldsUrl(),
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetCommonFieldsResponseSchema,
|
||||
});
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'GET',
|
||||
url: this.getIncidentFieldsUrl(),
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: GetCommonFieldsResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
const fields = res.data.map((field) => {
|
||||
return {
|
||||
|
|
|
@ -13,12 +13,20 @@ import {
|
|||
} from '../../../common/sentinelone/types';
|
||||
import { API_PATH } from './sentinelone';
|
||||
import { SentinelOneGetActivitiesResponseSchema } from '../../../common/sentinelone/schema';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
|
||||
describe('SentinelOne Connector', () => {
|
||||
let connectorInstance: ReturnType<typeof sentinelOneConnectorMocks.create>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
beforeEach(() => {
|
||||
connectorInstance = sentinelOneConnectorMocks.create();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchAgentFiles()', () => {
|
||||
|
@ -35,15 +43,17 @@ describe('SentinelOne Connector', () => {
|
|||
it('should error if no agent id provided', async () => {
|
||||
fetchAgentFilesParams.agentId = '';
|
||||
|
||||
await expect(connectorInstance.fetchAgentFiles(fetchAgentFilesParams)).rejects.toHaveProperty(
|
||||
'message',
|
||||
"'agentId' parameter is required"
|
||||
);
|
||||
await expect(
|
||||
connectorInstance.fetchAgentFiles(fetchAgentFilesParams, connectorUsageCollector)
|
||||
).rejects.toHaveProperty('message', "'agentId' parameter is required");
|
||||
});
|
||||
|
||||
it('should call SentinelOne fetch-files API with expected data', async () => {
|
||||
const fetchFilesUrl = `${connectorInstance.constructorParams.config.url}${API_PATH}/agents/${fetchAgentFilesParams.agentId}/actions/fetch-files`;
|
||||
const response = await connectorInstance.fetchAgentFiles(fetchAgentFilesParams);
|
||||
const response = await connectorInstance.fetchAgentFiles(
|
||||
fetchAgentFilesParams,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(response).toEqual({ data: { success: true }, errors: null });
|
||||
expect(connectorInstance.requestSpy).toHaveBeenLastCalledWith({
|
||||
|
@ -76,14 +86,14 @@ describe('SentinelOne Connector', () => {
|
|||
it('should error if called with invalid agent id', async () => {
|
||||
downloadAgentFileParams.agentId = '';
|
||||
await expect(
|
||||
connectorInstance.downloadAgentFile(downloadAgentFileParams)
|
||||
connectorInstance.downloadAgentFile(downloadAgentFileParams, connectorUsageCollector)
|
||||
).rejects.toHaveProperty('message', "'agentId' parameter is required");
|
||||
});
|
||||
|
||||
it('should call SentinelOne api with expected url', async () => {
|
||||
await expect(connectorInstance.downloadAgentFile(downloadAgentFileParams)).resolves.toEqual(
|
||||
connectorInstance.mockResponses.downloadAgentFileApiResponse
|
||||
);
|
||||
await expect(
|
||||
connectorInstance.downloadAgentFile(downloadAgentFileParams, connectorUsageCollector)
|
||||
).resolves.toEqual(connectorInstance.mockResponses.downloadAgentFileApiResponse);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -122,7 +132,10 @@ describe('SentinelOne Connector', () => {
|
|||
|
||||
describe('#downloadRemoteScriptResults()', () => {
|
||||
it('should call SentinelOne api to retrieve task results', async () => {
|
||||
await connectorInstance.downloadRemoteScriptResults({ taskId: 'task-123' });
|
||||
await connectorInstance.downloadRemoteScriptResults(
|
||||
{ taskId: 'task-123' },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(connectorInstance.requestSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -136,13 +149,19 @@ describe('SentinelOne Connector', () => {
|
|||
connectorInstance.mockResponses.getRemoteScriptResults.data.download_links = [];
|
||||
|
||||
await expect(
|
||||
connectorInstance.downloadRemoteScriptResults({ taskId: 'task-123' })
|
||||
connectorInstance.downloadRemoteScriptResults(
|
||||
{ taskId: 'task-123' },
|
||||
connectorUsageCollector
|
||||
)
|
||||
).rejects.toThrow('Download URL for script results of task id [task-123] not found');
|
||||
});
|
||||
|
||||
it('should return a Stream for downloading the file', async () => {
|
||||
await expect(
|
||||
connectorInstance.downloadRemoteScriptResults({ taskId: 'task-123' })
|
||||
connectorInstance.downloadRemoteScriptResults(
|
||||
{ taskId: 'task-123' },
|
||||
connectorUsageCollector
|
||||
)
|
||||
).resolves.toEqual(connectorInstance.mockResponses.downloadRemoteScriptResults);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { Stream } from 'stream';
|
||||
import type {
|
||||
SentinelOneConfig,
|
||||
|
@ -157,67 +158,94 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
});
|
||||
}
|
||||
|
||||
public async fetchAgentFiles({ files, agentId, zipPassCode }: SentinelOneFetchAgentFilesParams) {
|
||||
public async fetchAgentFiles(
|
||||
{ files, agentId, zipPassCode }: SentinelOneFetchAgentFilesParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
if (!agentId) {
|
||||
throw new Error(`'agentId' parameter is required`);
|
||||
}
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
url: `${this.urls.agents}/${agentId}/actions/fetch-files`,
|
||||
method: 'post',
|
||||
data: {
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: `${this.urls.agents}/${agentId}/actions/fetch-files`,
|
||||
method: 'post',
|
||||
data: {
|
||||
password: zipPassCode,
|
||||
files,
|
||||
data: {
|
||||
password: zipPassCode,
|
||||
files,
|
||||
},
|
||||
},
|
||||
responseSchema: SentinelOneFetchAgentFilesResponseSchema,
|
||||
},
|
||||
responseSchema: SentinelOneFetchAgentFilesResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async downloadAgentFile({ agentId, activityId }: SentinelOneDownloadAgentFileParams) {
|
||||
public async downloadAgentFile(
|
||||
{ agentId, activityId }: SentinelOneDownloadAgentFileParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
if (!agentId) {
|
||||
throw new Error(`'agentId' parameter is required`);
|
||||
}
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
url: `${this.urls.agents}/${agentId}/uploads/${activityId}`,
|
||||
method: 'get',
|
||||
responseType: 'stream',
|
||||
responseSchema: SentinelOneDownloadAgentFileResponseSchema,
|
||||
});
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: `${this.urls.agents}/${agentId}/uploads/${activityId}`,
|
||||
method: 'get',
|
||||
responseType: 'stream',
|
||||
responseSchema: SentinelOneDownloadAgentFileResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async getActivities(queryParams?: SentinelOneGetActivitiesParams) {
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.activities,
|
||||
method: 'get',
|
||||
params: queryParams,
|
||||
responseSchema: SentinelOneGetActivitiesResponseSchema,
|
||||
});
|
||||
public async getActivities(
|
||||
queryParams?: SentinelOneGetActivitiesParams,
|
||||
connectorUsageCollector?: ConnectorUsageCollector
|
||||
) {
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.activities,
|
||||
method: 'get',
|
||||
params: queryParams,
|
||||
responseSchema: SentinelOneGetActivitiesResponseSchema,
|
||||
},
|
||||
connectorUsageCollector!
|
||||
);
|
||||
}
|
||||
|
||||
public async executeScript({ filter, script }: SentinelOneExecuteScriptParams) {
|
||||
public async executeScript(
|
||||
{ filter, script }: SentinelOneExecuteScriptParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
if (!filter.ids && !filter.uuids) {
|
||||
throw new Error(`A filter must be defined; either 'ids' or 'uuids'`);
|
||||
}
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.remoteScriptsExecute,
|
||||
method: 'post',
|
||||
data: {
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.remoteScriptsExecute,
|
||||
method: 'post',
|
||||
data: {
|
||||
outputDestination: 'SentinelCloud',
|
||||
...script,
|
||||
data: {
|
||||
outputDestination: 'SentinelCloud',
|
||||
...script,
|
||||
},
|
||||
filter,
|
||||
},
|
||||
filter,
|
||||
responseSchema: SentinelOneExecuteScriptResponseSchema,
|
||||
},
|
||||
responseSchema: SentinelOneExecuteScriptResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async isolateHost({ alertIds, ...payload }: SentinelOneIsolateHostParams) {
|
||||
const response = await this.getAgents(payload);
|
||||
public async isolateHost(
|
||||
{ alertIds, ...payload }: SentinelOneIsolateHostParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const response = await this.getAgents(payload, connectorUsageCollector);
|
||||
|
||||
if (response.data.length === 0) {
|
||||
const errorMessage = 'No agents found';
|
||||
|
@ -233,20 +261,26 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
|
||||
const agentId = response.data[0].id;
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.isolateHost,
|
||||
method: 'post',
|
||||
data: {
|
||||
filter: {
|
||||
ids: agentId,
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.isolateHost,
|
||||
method: 'post',
|
||||
data: {
|
||||
filter: {
|
||||
ids: agentId,
|
||||
},
|
||||
},
|
||||
responseSchema: SentinelOneIsolateHostResponseSchema,
|
||||
},
|
||||
responseSchema: SentinelOneIsolateHostResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async releaseHost({ alertIds, ...payload }: SentinelOneIsolateHostParams) {
|
||||
const response = await this.getAgents(payload);
|
||||
public async releaseHost(
|
||||
{ alertIds, ...payload }: SentinelOneIsolateHostParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
const response = await this.getAgents(payload, connectorUsageCollector);
|
||||
|
||||
if (response.data.length === 0) {
|
||||
throw new Error('No agents found');
|
||||
|
@ -258,57 +292,76 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
|
||||
const agentId = response.data[0].id;
|
||||
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.releaseHost,
|
||||
method: 'post',
|
||||
data: {
|
||||
filter: {
|
||||
ids: agentId,
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.releaseHost,
|
||||
method: 'post',
|
||||
data: {
|
||||
filter: {
|
||||
ids: agentId,
|
||||
},
|
||||
},
|
||||
responseSchema: SentinelOneIsolateHostResponseSchema,
|
||||
},
|
||||
responseSchema: SentinelOneIsolateHostResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async getAgents(
|
||||
payload: SentinelOneGetAgentsParams
|
||||
payload: SentinelOneGetAgentsParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<SentinelOneGetAgentsResponse> {
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.agents,
|
||||
params: {
|
||||
...payload,
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.agents,
|
||||
params: {
|
||||
...payload,
|
||||
},
|
||||
responseSchema: SentinelOneGetAgentsResponseSchema,
|
||||
},
|
||||
responseSchema: SentinelOneGetAgentsResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async getRemoteScriptStatus(
|
||||
payload: SentinelOneGetRemoteScriptStatusParams
|
||||
payload: SentinelOneGetRemoteScriptStatusParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<SentinelOneGetRemoteScriptStatusApiResponse> {
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.remoteScriptStatus,
|
||||
params: {
|
||||
parent_task_id: payload.parentTaskId,
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.remoteScriptStatus,
|
||||
params: {
|
||||
parent_task_id: payload.parentTaskId,
|
||||
},
|
||||
responseSchema: SentinelOneGetRemoteScriptStatusResponseSchema,
|
||||
},
|
||||
responseSchema: SentinelOneGetRemoteScriptStatusResponseSchema,
|
||||
}) as unknown as SentinelOneGetRemoteScriptStatusApiResponse;
|
||||
connectorUsageCollector
|
||||
) as unknown as SentinelOneGetRemoteScriptStatusApiResponse;
|
||||
}
|
||||
|
||||
public async getRemoteScriptResults({
|
||||
taskIds,
|
||||
}: SentinelOneGetRemoteScriptResultsParams): Promise<SentinelOneGetRemoteScriptResultsApiResponse> {
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.remoteScriptsResults,
|
||||
method: 'post',
|
||||
data: { data: { taskIds } },
|
||||
responseSchema: SentinelOneGetRemoteScriptResultsResponseSchema,
|
||||
}) as unknown as SentinelOneGetRemoteScriptResultsApiResponse;
|
||||
public async getRemoteScriptResults(
|
||||
{ taskIds }: SentinelOneGetRemoteScriptResultsParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<SentinelOneGetRemoteScriptResultsApiResponse> {
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.remoteScriptsResults,
|
||||
method: 'post',
|
||||
data: { data: { taskIds } },
|
||||
responseSchema: SentinelOneGetRemoteScriptResultsResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
) as unknown as SentinelOneGetRemoteScriptResultsApiResponse;
|
||||
}
|
||||
|
||||
public async downloadRemoteScriptResults({
|
||||
taskId,
|
||||
}: SentinelOneDownloadRemoteScriptResultsParams): Promise<Stream> {
|
||||
const scriptResultsInfo = await this.getRemoteScriptResults({ taskIds: [taskId] });
|
||||
public async downloadRemoteScriptResults(
|
||||
{ taskId }: SentinelOneDownloadRemoteScriptResultsParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<Stream> {
|
||||
const scriptResultsInfo = await this.getRemoteScriptResults(
|
||||
{ taskIds: [taskId] },
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
this.logger.debug(
|
||||
() => `script results for taskId [${taskId}]:\n${JSON.stringify(scriptResultsInfo)}`
|
||||
|
@ -327,26 +380,33 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
throw new Error(`Download URL for script results of task id [${taskId}] not found`);
|
||||
}
|
||||
|
||||
const downloadConnection = await this.request({
|
||||
url: fileUrl,
|
||||
method: 'get',
|
||||
responseType: 'stream',
|
||||
responseSchema: SentinelOneDownloadRemoteScriptResultsResponseSchema,
|
||||
});
|
||||
const downloadConnection = await this.request(
|
||||
{
|
||||
url: fileUrl,
|
||||
method: 'get',
|
||||
responseType: 'stream',
|
||||
responseSchema: SentinelOneDownloadRemoteScriptResultsResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return downloadConnection.data;
|
||||
}
|
||||
|
||||
private async sentinelOneApiRequest<R extends SentinelOneBaseApiResponse>(
|
||||
req: SubActionRequestParams<R>
|
||||
req: SubActionRequestParams<R>,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<R> {
|
||||
const response = await this.request<R>({
|
||||
...req,
|
||||
params: {
|
||||
...req.params,
|
||||
APIToken: this.secrets.token,
|
||||
const response = await this.request<R>(
|
||||
{
|
||||
...req,
|
||||
params: {
|
||||
...req.params,
|
||||
APIToken: this.secrets.token,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
@ -374,15 +434,19 @@ export class SentinelOneConnector extends SubActionConnector<
|
|||
}
|
||||
|
||||
public async getRemoteScripts(
|
||||
payload: SentinelOneGetRemoteScriptsParams
|
||||
payload: SentinelOneGetRemoteScriptsParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<SentinelOneGetRemoteScriptsResponse> {
|
||||
return this.sentinelOneApiRequest({
|
||||
url: this.urls.remoteScripts,
|
||||
params: {
|
||||
limit: API_MAX_RESULTS,
|
||||
...payload,
|
||||
return this.sentinelOneApiRequest(
|
||||
{
|
||||
url: this.urls.remoteScripts,
|
||||
params: {
|
||||
limit: API_MAX_RESULTS,
|
||||
...payload,
|
||||
},
|
||||
responseSchema: SentinelOneGetRemoteScriptsResponseSchema,
|
||||
},
|
||||
responseSchema: SentinelOneGetRemoteScriptsResponseSchema,
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { validateParams } from '@kbn/actions-plugin/server/lib';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { getConnectorType, ServerLogConnectorType, ServerLogConnectorTypeExecutorOptions } from '.';
|
||||
|
@ -107,6 +108,10 @@ describe('execute()', () => {
|
|||
secrets: {},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector: new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
}),
|
||||
};
|
||||
await connectorType.executor(executorOptions);
|
||||
expect(mockedLogger.info).toHaveBeenCalledWith('Server log: message text here');
|
||||
|
|
|
@ -102,7 +102,15 @@ async function executorITOM(
|
|||
ExecutorParamsITOM
|
||||
>
|
||||
): Promise<ConnectorTypeExecutorResult<ServiceNowExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions;
|
||||
const {
|
||||
actionId,
|
||||
config,
|
||||
params,
|
||||
secrets,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
} = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
const connectorTokenClient = execOptions.services.connectorTokenClient;
|
||||
const externalServiceConfig = snExternalServiceConfig[actionTypeId];
|
||||
|
@ -119,6 +127,7 @@ async function executorITOM(
|
|||
serviceConfig: externalServiceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn: createService,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const apiAsRecord = api as unknown as Record<string, unknown>;
|
||||
|
|
|
@ -15,6 +15,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks';
|
|||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { snExternalServiceConfig } from '../lib/servicenow/config';
|
||||
import { itomEventParams, serviceNowChoices } from '../lib/servicenow/mocks';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
|
@ -33,8 +34,13 @@ const configurationUtilities = actionsConfigMock.create();
|
|||
|
||||
describe('ServiceNow SIR service', () => {
|
||||
let service: ExternalServiceITOM;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
|
@ -44,6 +50,7 @@ describe('ServiceNow SIR service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-itom'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
}) as ExternalServiceITOM;
|
||||
});
|
||||
|
||||
|
@ -69,6 +76,7 @@ describe('ServiceNow SIR service', () => {
|
|||
url: 'https://example.com/api/global/em/jsonv2',
|
||||
method: 'post',
|
||||
data: { records: [itomEventParams] },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -85,6 +93,7 @@ describe('ServiceNow SIR service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=em_event^element=severity^language=en&sysparm_fields=label,value,dependent_value,element',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ export const createExternalService: ServiceFactory<ExternalServiceITOM> = ({
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
connectorUsageCollector,
|
||||
}): ExternalServiceITOM => {
|
||||
const snService = createExternalServiceCommon({
|
||||
credentials,
|
||||
|
@ -30,6 +31,7 @@ export const createExternalService: ServiceFactory<ExternalServiceITOM> = ({
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const addEvent = async (params: ExecutorSubActionAddEventParams) => {
|
||||
|
@ -41,6 +43,7 @@ export const createExternalService: ServiceFactory<ExternalServiceITOM> = ({
|
|||
method: 'post',
|
||||
data: { records: [params] },
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
snService.checkInstance(res);
|
||||
|
|
|
@ -125,8 +125,16 @@ async function executor(
|
|||
ExecutorParams
|
||||
>
|
||||
): Promise<ConnectorTypeExecutorResult<ServiceNowExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets, services, configurationUtilities, logger } =
|
||||
execOptions;
|
||||
const {
|
||||
actionId,
|
||||
config,
|
||||
params,
|
||||
secrets,
|
||||
services,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
} = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
const connectorTokenClient = services.connectorTokenClient;
|
||||
const externalServiceConfig = snExternalServiceConfig[actionTypeId];
|
||||
|
@ -143,6 +151,7 @@ async function executor(
|
|||
serviceConfig: externalServiceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn: createService,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const apiAsRecord = api as unknown as Record<string, unknown>;
|
||||
|
|
|
@ -15,6 +15,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks';
|
|||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { serviceNowCommonFields, serviceNowChoices } from '../lib/servicenow/mocks';
|
||||
import { snExternalServiceConfig } from '../lib/servicenow/config';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
jest.mock('axios', () => ({
|
||||
|
@ -122,6 +123,7 @@ const expectImportedIncident = (update: boolean) => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -135,6 +137,7 @@ const expectImportedIncident = (update: boolean) => {
|
|||
u_description: 'desc',
|
||||
...(update ? { elastic_incident_id: '1' } : {}),
|
||||
},
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -143,14 +146,20 @@ const expectImportedIncident = (update: boolean) => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
};
|
||||
|
||||
describe('ServiceNow service', () => {
|
||||
let service: ExternalService;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
// The trailing slash at the end of the url is intended.
|
||||
|
@ -162,6 +171,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -177,6 +187,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -217,6 +228,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -381,6 +393,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -408,6 +421,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -421,6 +435,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
@ -434,6 +449,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -487,6 +503,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const res = await createIncident(service);
|
||||
|
@ -497,6 +514,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -506,6 +524,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
|
||||
method: 'post',
|
||||
data: { u_short_description: 'title', u_description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -514,6 +533,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -572,6 +592,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -596,6 +617,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/incident',
|
||||
method: 'post',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -609,6 +631,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
|
@ -624,6 +647,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/sn_si_incident',
|
||||
method: 'post',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -660,6 +684,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const res = await updateIncident(service);
|
||||
|
@ -669,6 +694,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(2, {
|
||||
|
@ -678,6 +704,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident',
|
||||
method: 'post',
|
||||
data: { u_short_description: 'title', u_description: 'desc', elastic_incident_id: '1' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenNthCalledWith(3, {
|
||||
|
@ -686,6 +713,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'get',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -747,6 +775,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -772,6 +801,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/incident/1',
|
||||
method: 'patch',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -785,6 +815,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
mockIncidentResponse(false);
|
||||
|
@ -801,6 +832,7 @@ describe('ServiceNow service', () => {
|
|||
url: 'https://example.com/api/now/v2/table/sn_si_incident/1',
|
||||
method: 'patch',
|
||||
data: { short_description: 'title', description: 'desc' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1');
|
||||
|
@ -820,6 +852,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -841,6 +874,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
@ -853,6 +887,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -889,6 +924,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category^language=en&sysparm_fields=label,value,dependent_value,element',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -910,6 +946,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
requestMock.mockImplementation(() => ({
|
||||
|
@ -923,6 +960,7 @@ describe('ServiceNow service', () => {
|
|||
logger,
|
||||
configurationUtilities,
|
||||
url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category^language=en&sysparm_fields=label,value,dependent_value,element',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1015,6 +1053,7 @@ describe('ServiceNow service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: { ...snExternalServiceConfig['.servicenow'], useImportAPI: false },
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
await service.checkIfApplicationIsInstalled();
|
||||
expect(requestMock).not.toHaveBeenCalled();
|
||||
|
|
|
@ -116,8 +116,16 @@ async function executor(
|
|||
ExecutorParams
|
||||
>
|
||||
): Promise<ConnectorTypeExecutorResult<ServiceNowExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets, services, configurationUtilities, logger } =
|
||||
execOptions;
|
||||
const {
|
||||
actionId,
|
||||
config,
|
||||
params,
|
||||
secrets,
|
||||
services,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
} = execOptions;
|
||||
const { subAction, subActionParams } = params;
|
||||
const connectorTokenClient = services.connectorTokenClient;
|
||||
const externalServiceConfig = snExternalServiceConfig[actionTypeId];
|
||||
|
@ -134,6 +142,7 @@ async function executor(
|
|||
serviceConfig: externalServiceConfig,
|
||||
connectorTokenClient,
|
||||
createServiceFn: createService,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const apiAsRecord = api as unknown as Record<string, unknown>;
|
||||
|
|
|
@ -15,6 +15,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks';
|
|||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { observables } from '../lib/servicenow/mocks';
|
||||
import { snExternalServiceConfig } from '../lib/servicenow/config';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
|
@ -31,6 +32,7 @@ jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
|||
axios.create = jest.fn(() => axios);
|
||||
const requestMock = utils.request as jest.Mock;
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
const mockApplicationVersion = () =>
|
||||
requestMock.mockImplementationOnce(() => ({
|
||||
|
@ -70,6 +72,7 @@ const expectAddObservables = (single: boolean) => {
|
|||
configurationUtilities,
|
||||
url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health',
|
||||
method: 'get',
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
|
||||
const url = single
|
||||
|
@ -85,6 +88,7 @@ const expectAddObservables = (single: boolean) => {
|
|||
url,
|
||||
method: 'post',
|
||||
data,
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -92,6 +96,10 @@ describe('ServiceNow SIR service', () => {
|
|||
let service: ExternalServiceSIR;
|
||||
|
||||
beforeEach(() => {
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
service = createExternalService({
|
||||
credentials: {
|
||||
config: { apiUrl: 'https://example.com/', isOAuth: false },
|
||||
|
@ -101,6 +109,7 @@ describe('ServiceNow SIR service', () => {
|
|||
configurationUtilities,
|
||||
serviceConfig: snExternalServiceConfig['.servicenow-sir'],
|
||||
axiosInstance: axios,
|
||||
connectorUsageCollector,
|
||||
}) as ExternalServiceSIR;
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export const createExternalService: ServiceFactory<ExternalServiceSIR> = ({
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
connectorUsageCollector,
|
||||
}): ExternalServiceSIR => {
|
||||
const snService = createExternalServiceCommon({
|
||||
credentials,
|
||||
|
@ -35,6 +36,7 @@ export const createExternalService: ServiceFactory<ExternalServiceSIR> = ({
|
|||
configurationUtilities,
|
||||
serviceConfig,
|
||||
axiosInstance,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const _addObservable = async (data: Observable | Observable[], url: string) => {
|
||||
|
@ -47,6 +49,7 @@ export const createExternalService: ServiceFactory<ExternalServiceSIR> = ({
|
|||
method: 'post',
|
||||
data,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
snService.checkInstance(res);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Logger } from '@kbn/core/server';
|
|||
import {
|
||||
Services,
|
||||
ActionTypeExecutorResult as ConnectorTypeExecutorResult,
|
||||
ConnectorUsageCollector,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
import { validateParams, validateSecrets } from '@kbn/actions-plugin/server/lib';
|
||||
import {
|
||||
|
@ -35,6 +36,7 @@ const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
|||
|
||||
let connectorType: SlackConnectorType;
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
|
@ -43,6 +45,10 @@ beforeEach(() => {
|
|||
return { status: 'ok', actionId: options.actionId };
|
||||
},
|
||||
});
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('connector registration', () => {
|
||||
|
@ -181,6 +187,7 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -201,6 +208,7 @@ describe('execute()', () => {
|
|||
params: { message: 'failure: this invocation should fail' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"slack mockExecutor failure: this invocation should fail"`
|
||||
|
@ -226,6 +234,7 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
|
@ -252,6 +261,7 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
|
@ -278,6 +288,7 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
|
@ -304,6 +315,7 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
|
@ -330,6 +342,7 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities: configUtils,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
|
|
|
@ -139,7 +139,8 @@ function validateConnectorTypeConfig(
|
|||
async function slackExecutor(
|
||||
execOptions: SlackConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> {
|
||||
const { actionId, secrets, params, configurationUtilities, logger } = execOptions;
|
||||
const { actionId, secrets, params, configurationUtilities, logger, connectorUsageCollector } =
|
||||
execOptions;
|
||||
|
||||
let result: IncomingWebhookResult;
|
||||
const { webhookUrl } = secrets;
|
||||
|
@ -163,6 +164,7 @@ async function slackExecutor(
|
|||
const webhook = new IncomingWebhook(webhookUrl, {
|
||||
agent,
|
||||
});
|
||||
connectorUsageCollector.addRequestBodyBytes(undefined, { text: message });
|
||||
result = await webhook.send(message);
|
||||
} catch (err) {
|
||||
if (err.original == null || err.original.response == null) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import axios from 'axios';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, Services } from '@kbn/actions-plugin/server/types';
|
||||
import { validateConfig, validateParams, validateSecrets } from '@kbn/actions-plugin/server/lib';
|
||||
import { getConnectorType } from '.';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
|
@ -39,10 +39,15 @@ const headers = {
|
|||
|
||||
let connectorType: SlackApiConnectorType;
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('connector registration', () => {
|
||||
|
@ -198,6 +203,7 @@ describe('execute', () => {
|
|||
params: {} as PostMessageParams,
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"[Action][ExternalService] -> [Slack API] Unsupported subAction type undefined."`
|
||||
|
@ -296,6 +302,7 @@ describe('execute', () => {
|
|||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -306,6 +313,7 @@ describe('execute', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'general', text: 'some text' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
|
@ -386,6 +394,7 @@ describe('execute', () => {
|
|||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -396,6 +405,7 @@ describe('execute', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'LKJHGF345', text: 'some text' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
|
@ -476,6 +486,7 @@ describe('execute', () => {
|
|||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -486,6 +497,7 @@ describe('execute', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'LKJHGF345', blocks: testBlock.blocks },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
|
@ -525,6 +537,7 @@ describe('execute', () => {
|
|||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(requestMock).toHaveBeenCalledWith({
|
||||
|
@ -534,6 +547,7 @@ describe('execute', () => {
|
|||
logger: mockedLogger,
|
||||
method: 'get',
|
||||
url: 'https://slack.com/api/conversations.info?channel=ZXCVBNM567',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
|
|
|
@ -107,6 +107,7 @@ const slackApiExecutor = async ({
|
|||
secrets,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
}: SlackApiExecutorOptions): Promise<ActionTypeExecutorResult<unknown>> => {
|
||||
const subAction = params.subAction;
|
||||
|
||||
|
@ -128,7 +129,8 @@ const slackApiExecutor = async ({
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
if (subAction === 'validChannelId') {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.moc
|
|||
import { createExternalService } from './service';
|
||||
import { SlackApiService } from '../../../common/slack_api/types';
|
||||
import { SLACK_API_CONNECTOR_ID } from '../../../common/slack_api/constants';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
|
@ -28,6 +29,7 @@ jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
|||
axios.create = jest.fn(() => axios);
|
||||
const requestMock = request as jest.Mock;
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
const channel = {
|
||||
id: 'channel_id_1',
|
||||
|
@ -117,12 +119,17 @@ describe('Slack API service', () => {
|
|||
let service: SlackApiService;
|
||||
|
||||
beforeAll(() => {
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
service = createExternalService(
|
||||
{
|
||||
secrets: { token: 'token' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -138,7 +145,8 @@ describe('Slack API service', () => {
|
|||
secrets: { token: '' },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"[Action][Slack API]: Wrong configuration."`);
|
||||
});
|
||||
|
@ -172,6 +180,7 @@ describe('Slack API service', () => {
|
|||
configurationUtilities,
|
||||
method: 'get',
|
||||
url: 'https://slack.com/api/conversations.info?channel=channel_id_1',
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -207,6 +216,7 @@ describe('Slack API service', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'general', text: 'a message' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -231,6 +241,7 @@ describe('Slack API service', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'QWEERTYU987', text: 'a message' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -251,6 +262,7 @@ describe('Slack API service', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'QWEERTYU987', text: 'a message' },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -291,6 +303,7 @@ describe('Slack API service', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'general', blocks: testBlock.blocks },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -315,6 +328,7 @@ describe('Slack API service', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'QWEERTYU987', blocks: testBlock.blocks },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -338,6 +352,7 @@ describe('Slack API service', () => {
|
|||
method: 'post',
|
||||
url: 'https://slack.com/api/chat.postMessage',
|
||||
data: { channel: 'QWEERTYU987', blocks: testBlock.blocks },
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, getOrElse } from 'fp-ts/lib/Option';
|
||||
import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { SLACK_CONNECTOR_NAME } from './translations';
|
||||
import type {
|
||||
PostMessageSubActionParams,
|
||||
|
@ -111,7 +112,8 @@ export const createExternalService = (
|
|||
secrets: { token: string };
|
||||
},
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): SlackApiService => {
|
||||
const { token } = secrets;
|
||||
const { allowedChannels } = config || { allowedChannels: [] };
|
||||
|
@ -139,6 +141,7 @@ export const createExternalService = (
|
|||
method: 'get',
|
||||
headers,
|
||||
url: `${SLACK_URL}conversations.info?channel=${channelId}`,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
};
|
||||
if (channelId.length === 0) {
|
||||
|
@ -207,6 +210,7 @@ export const createExternalService = (
|
|||
data: { channel: channelToUse, text },
|
||||
headers,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
return buildSlackExecutorSuccessResponse({ slackApiResponseData: result.data });
|
||||
|
@ -232,6 +236,7 @@ export const createExternalService = (
|
|||
data: { channel: channelToUse, blocks: blockJson.blocks },
|
||||
headers,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
return buildSlackExecutorSuccessResponse({ slackApiResponseData: result.data });
|
||||
|
|
|
@ -76,7 +76,15 @@ async function executor(
|
|||
ExecutorParams
|
||||
>
|
||||
): Promise<ConnectorTypeExecutorResult<SwimlaneExecutorResultData | {}>> {
|
||||
const { actionId, config, params, secrets, configurationUtilities, logger } = execOptions;
|
||||
const {
|
||||
actionId,
|
||||
config,
|
||||
params,
|
||||
secrets,
|
||||
configurationUtilities,
|
||||
logger,
|
||||
connectorUsageCollector,
|
||||
} = execOptions;
|
||||
const { subAction, subActionParams } = params as ExecutorParams;
|
||||
let data: SwimlaneExecutorResultData | null = null;
|
||||
|
||||
|
@ -86,7 +94,8 @@ async function executor(
|
|||
secrets,
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
if (!api[subAction]) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { request, createAxiosResponse } from '@kbn/actions-plugin/server/lib/axi
|
|||
import { createExternalService } from './service';
|
||||
import { mappings } from './mocks';
|
||||
import { ExternalService } from './types';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
|
@ -56,8 +57,13 @@ describe('Swimlane Service', () => {
|
|||
};
|
||||
|
||||
const url = config.apiUrl.slice(0, -1);
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeAll(() => {
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
service = createExternalService(
|
||||
{
|
||||
// The trailing slash at the end of the url is intended.
|
||||
|
@ -66,7 +72,8 @@ describe('Swimlane Service', () => {
|
|||
secrets: { apiToken },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
|
@ -87,7 +94,8 @@ describe('Swimlane Service', () => {
|
|||
secrets: { apiToken },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -104,7 +112,8 @@ describe('Swimlane Service', () => {
|
|||
secrets: { apiToken },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -122,7 +131,8 @@ describe('Swimlane Service', () => {
|
|||
secrets: { apiToken },
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
)
|
||||
).toThrow();
|
||||
});
|
||||
|
@ -138,7 +148,8 @@ describe('Swimlane Service', () => {
|
|||
},
|
||||
},
|
||||
logger,
|
||||
configurationUtilities
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
}).toThrow();
|
||||
});
|
||||
|
@ -191,6 +202,7 @@ describe('Swimlane Service', () => {
|
|||
url: `${url}/api/app/${config.appId}/record`,
|
||||
method: 'post',
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -274,6 +286,7 @@ describe('Swimlane Service', () => {
|
|||
url: `${url}/api/app/${config.appId}/record/${incidentId}`,
|
||||
method: 'patch',
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -353,6 +366,7 @@ describe('Swimlane Service', () => {
|
|||
url: `${url}/api/app/${config.appId}/record/${incidentId}/${mappings.commentsConfig.id}/comment`,
|
||||
method: 'post',
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
throwIfResponseIsNotValid,
|
||||
} from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { getBodyForEventAction } from './helpers';
|
||||
import {
|
||||
CreateCommentParams,
|
||||
|
@ -42,7 +43,8 @@ const createErrorMessage = (errorResponse: ResponseError | null | undefined): st
|
|||
export const createExternalService = (
|
||||
{ config, secrets }: ExternalServiceCredentials,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): ExternalService => {
|
||||
const { apiUrl: url, appId, mappings } = config as SwimlanePublicConfigurationType;
|
||||
const { apiToken } = secrets as SwimlaneSecretConfigurationType;
|
||||
|
@ -92,6 +94,7 @@ export const createExternalService = (
|
|||
logger,
|
||||
method: 'post',
|
||||
url: getPostRecordUrl(appId),
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -132,6 +135,7 @@ export const createExternalService = (
|
|||
logger,
|
||||
method: 'patch',
|
||||
url: getPostRecordIdUrl(appId, params.incidentId),
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
throwIfResponseIsNotValid({
|
||||
|
@ -181,6 +185,7 @@ export const createExternalService = (
|
|||
logger,
|
||||
method: 'post',
|
||||
url: getPostCommentUrl(appId, incidentId, fieldId),
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, Services } from '@kbn/actions-plugin/server/types';
|
||||
import { validateParams, validateSecrets } from '@kbn/actions-plugin/server/lib';
|
||||
import axios from 'axios';
|
||||
import { getConnectorType, TeamsConnectorType, ConnectorTypeId } from '.';
|
||||
|
@ -34,10 +34,15 @@ const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
|||
|
||||
let connectorType: TeamsConnectorType;
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('connector registration', () => {
|
||||
|
@ -167,11 +172,42 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
delete requestMock.mock.calls[0][0].configurationUtilities;
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from teams action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"text": "this invocation should succeed",
|
||||
},
|
||||
|
@ -223,11 +259,42 @@ describe('execute()', () => {
|
|||
params: { message: 'this invocation should succeed' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
delete requestMock.mock.calls[0][0].configurationUtilities;
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from teams action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"text": "this invocation should succeed",
|
||||
},
|
||||
|
|
|
@ -119,7 +119,8 @@ function validateConnectorTypeConfig(
|
|||
async function teamsExecutor(
|
||||
execOptions: TeamsConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> {
|
||||
const { actionId, secrets, params, configurationUtilities, logger } = execOptions;
|
||||
const { actionId, secrets, params, configurationUtilities, logger, connectorUsageCollector } =
|
||||
execOptions;
|
||||
const { webhookUrl } = secrets;
|
||||
const { message } = params;
|
||||
const data = { text: message };
|
||||
|
@ -134,6 +135,7 @@ async function teamsExecutor(
|
|||
logger,
|
||||
data,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -18,17 +18,20 @@ import {
|
|||
PushToServiceIncidentSchema,
|
||||
} from '../../../common/thehive/schema';
|
||||
import type { ExecutorSubActionCreateAlertParams, Incident } from '../../../common/thehive/types';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const mockTime = new Date('2024-04-03T09:10:30.000');
|
||||
|
||||
describe('TheHiveConnector', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
const connector = new TheHiveConnector(
|
||||
{
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
connector: { id: '1', type: THEHIVE_CONNECTOR_ID },
|
||||
config: { url: 'https://example.com', organisation: null },
|
||||
secrets: { apiKey: 'test123' },
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
},
|
||||
PushToServiceIncidentSchema
|
||||
|
@ -36,6 +39,7 @@ describe('TheHiveConnector', () => {
|
|||
|
||||
let mockRequest: jest.Mock;
|
||||
let mockError: jest.Mock;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
|
@ -51,6 +55,10 @@ describe('TheHiveConnector', () => {
|
|||
throw new Error('API Error');
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIncident', () => {
|
||||
|
@ -124,18 +132,21 @@ describe('TheHiveConnector', () => {
|
|||
};
|
||||
|
||||
it('TheHive API call is successful with correct parameters', async () => {
|
||||
const response = await connector.createIncident(incident);
|
||||
const response = await connector.createIncident(incident, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/api/v1/case',
|
||||
method: 'post',
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
data: incident,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://example.com/api/v1/case',
|
||||
method: 'post',
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
data: incident,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({
|
||||
id: '~172064',
|
||||
url: 'https://example.com/cases/~172064/details',
|
||||
|
@ -148,7 +159,9 @@ describe('TheHiveConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.createIncident(incident)).rejects.toThrow('API Error');
|
||||
await expect(connector.createIncident(incident, connectorUsageCollector)).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -173,18 +186,24 @@ describe('TheHiveConnector', () => {
|
|||
};
|
||||
|
||||
it('TheHive API call is successful with correct parameters', async () => {
|
||||
const response = await connector.updateIncident({ incidentId: '~172064', incident });
|
||||
const response = await connector.updateIncident(
|
||||
{ incidentId: '~172064', incident },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/api/v1/case/~172064',
|
||||
method: 'patch',
|
||||
responseSchema: TheHiveUpdateIncidentResponseSchema,
|
||||
data: incident,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://example.com/api/v1/case/~172064',
|
||||
method: 'patch',
|
||||
responseSchema: TheHiveUpdateIncidentResponseSchema,
|
||||
data: incident,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual({
|
||||
id: '~172064',
|
||||
url: 'https://example.com/cases/~172064/details',
|
||||
|
@ -197,9 +216,9 @@ describe('TheHiveConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.updateIncident({ incidentId: '~172064', incident })).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
await expect(
|
||||
connector.updateIncident({ incidentId: '~172064', incident }, connectorUsageCollector)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -224,21 +243,27 @@ describe('TheHiveConnector', () => {
|
|||
});
|
||||
|
||||
it('TheHive API call is successful with correct parameters', async () => {
|
||||
await connector.addComment({
|
||||
incidentId: '~172064',
|
||||
comment: 'test comment',
|
||||
});
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/api/v1/case/~172064/comment',
|
||||
method: 'post',
|
||||
responseSchema: TheHiveAddCommentResponseSchema,
|
||||
data: { message: 'test comment' },
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
await connector.addComment(
|
||||
{
|
||||
incidentId: '~172064',
|
||||
comment: 'test comment',
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://example.com/api/v1/case/~172064/comment',
|
||||
method: 'post',
|
||||
responseSchema: TheHiveAddCommentResponseSchema,
|
||||
data: { message: 'test comment' },
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
},
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('errors during API calls are properly handled', async () => {
|
||||
|
@ -246,7 +271,10 @@ describe('TheHiveConnector', () => {
|
|||
connector.request = mockError;
|
||||
|
||||
await expect(
|
||||
connector.addComment({ incidentId: '~172064', comment: 'test comment' })
|
||||
connector.addComment(
|
||||
{ incidentId: '~172064', comment: 'test comment' },
|
||||
connectorUsageCollector
|
||||
)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
@ -314,16 +342,19 @@ describe('TheHiveConnector', () => {
|
|||
});
|
||||
|
||||
it('TheHive API call is successful with correct parameters', async () => {
|
||||
const response = await connector.getIncident({ id: '~172064' });
|
||||
const response = await connector.getIncident({ id: '~172064' }, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/api/v1/case/~172064',
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://example.com/api/v1/case/~172064',
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
|
@ -331,7 +362,9 @@ describe('TheHiveConnector', () => {
|
|||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.getIncident({ id: '~172064' })).rejects.toThrow('API Error');
|
||||
await expect(
|
||||
connector.getIncident({ id: '~172064' }, connectorUsageCollector)
|
||||
).rejects.toThrow('API Error');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -385,25 +418,30 @@ describe('TheHiveConnector', () => {
|
|||
};
|
||||
|
||||
it('TheHive API call is successful with correct parameters', async () => {
|
||||
await connector.createAlert(alert);
|
||||
await connector.createAlert(alert, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://example.com/api/v1/alert',
|
||||
method: 'post',
|
||||
responseSchema: TheHiveCreateAlertResponseSchema,
|
||||
data: alert,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
{
|
||||
url: 'https://example.com/api/v1/alert',
|
||||
method: 'post',
|
||||
responseSchema: TheHiveCreateAlertResponseSchema,
|
||||
data: alert,
|
||||
headers: {
|
||||
Authorization: 'Bearer test123',
|
||||
'X-Organisation': null,
|
||||
},
|
||||
},
|
||||
});
|
||||
connectorUsageCollector
|
||||
);
|
||||
});
|
||||
|
||||
it('errors during API calls are properly handled', async () => {
|
||||
// @ts-ignore
|
||||
connector.request = mockError;
|
||||
|
||||
await expect(connector.createAlert(alert)).rejects.toThrow('API Error');
|
||||
await expect(connector.createAlert(alert, connectorUsageCollector)).rejects.toThrow(
|
||||
'API Error'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { ServiceParams, CaseConnector } from '@kbn/actions-plugin/server';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { Type } from '@kbn/config-schema';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { SUB_ACTION } from '../../../common/thehive/constants';
|
||||
import {
|
||||
TheHiveIncidentResponseSchema,
|
||||
|
@ -68,14 +69,20 @@ export class TheHiveConnector extends CaseConnector<
|
|||
return `API Error: ${error.response?.data?.type} - ${error.response?.data?.message}`;
|
||||
}
|
||||
|
||||
public async createIncident(incident: Incident): Promise<ExternalServiceIncidentResponse> {
|
||||
const res = await this.request({
|
||||
method: 'post',
|
||||
url: `${this.url}/api/${API_VERSION}/case`,
|
||||
data: incident,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
});
|
||||
public async createIncident(
|
||||
incident: Incident,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<ExternalServiceIncidentResponse> {
|
||||
const res = await this.request(
|
||||
{
|
||||
method: 'post',
|
||||
url: `${this.url}/api/${API_VERSION}/case`,
|
||||
data: incident,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return {
|
||||
id: res.data._id,
|
||||
|
@ -85,30 +92,42 @@ export class TheHiveConnector extends CaseConnector<
|
|||
};
|
||||
}
|
||||
|
||||
public async addComment({ incidentId, comment }: { incidentId: string; comment: string }) {
|
||||
await this.request({
|
||||
method: 'post',
|
||||
url: `${this.url}/api/${API_VERSION}/case/${incidentId}/comment`,
|
||||
data: { message: comment },
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveAddCommentResponseSchema,
|
||||
});
|
||||
public async addComment(
|
||||
{ incidentId, comment }: { incidentId: string; comment: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
await this.request(
|
||||
{
|
||||
method: 'post',
|
||||
url: `${this.url}/api/${API_VERSION}/case/${incidentId}/comment`,
|
||||
data: { message: comment },
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveAddCommentResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async updateIncident({
|
||||
incidentId,
|
||||
incident,
|
||||
}: {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
}): Promise<ExternalServiceIncidentResponse> {
|
||||
await this.request({
|
||||
method: 'patch',
|
||||
url: `${this.url}/api/${API_VERSION}/case/${incidentId}`,
|
||||
data: incident,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveUpdateIncidentResponseSchema,
|
||||
});
|
||||
public async updateIncident(
|
||||
{
|
||||
incidentId,
|
||||
incident,
|
||||
}: {
|
||||
incidentId: string;
|
||||
incident: Incident;
|
||||
},
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<ExternalServiceIncidentResponse> {
|
||||
await this.request(
|
||||
{
|
||||
method: 'patch',
|
||||
url: `${this.url}/api/${API_VERSION}/case/${incidentId}`,
|
||||
data: incident,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveUpdateIncidentResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return {
|
||||
id: incidentId,
|
||||
|
@ -118,23 +137,35 @@ export class TheHiveConnector extends CaseConnector<
|
|||
};
|
||||
}
|
||||
|
||||
public async getIncident({ id }: { id: string }): Promise<GetIncidentResponse> {
|
||||
const res = await this.request({
|
||||
url: `${this.url}/api/${API_VERSION}/case/${id}`,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
});
|
||||
public async getIncident(
|
||||
{ id }: { id: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<GetIncidentResponse> {
|
||||
const res = await this.request(
|
||||
{
|
||||
url: `${this.url}/api/${API_VERSION}/case/${id}`,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveIncidentResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
public async createAlert(alert: ExecutorSubActionCreateAlertParams) {
|
||||
await this.request({
|
||||
method: 'post',
|
||||
url: `${this.url}/api/${API_VERSION}/alert`,
|
||||
data: alert,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveCreateAlertResponseSchema,
|
||||
});
|
||||
public async createAlert(
|
||||
alert: ExecutorSubActionCreateAlertParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
await this.request(
|
||||
{
|
||||
method: 'post',
|
||||
url: `${this.url}/api/${API_VERSION}/alert`,
|
||||
data: alert,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TheHiveCreateAlertResponseSchema,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
|||
import { TinesConnector } from './tines';
|
||||
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { API_MAX_RESULTS, TINES_CONNECTOR_ID } from '../../../common/tines/constants';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
jest.mock('axios');
|
||||
(axios as jest.Mocked<typeof axios>).create.mockImplementation(
|
||||
|
@ -78,6 +79,7 @@ const storiesGetRequestExpected = {
|
|||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { per_page: API_MAX_RESULTS },
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
};
|
||||
|
||||
const agentsGetRequestExpected = {
|
||||
|
@ -91,20 +93,28 @@ const agentsGetRequestExpected = {
|
|||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { story_id: story.id, per_page: API_MAX_RESULTS },
|
||||
connectorUsageCollector: expect.any(ConnectorUsageCollector),
|
||||
};
|
||||
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
describe('TinesConnector', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
const connector = new TinesConnector({
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
config: { url },
|
||||
connector: { id: '1', type: TINES_CONNECTOR_ID },
|
||||
secrets: { email, token },
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
logger,
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStories', () => {
|
||||
|
@ -113,13 +123,13 @@ describe('TinesConnector', () => {
|
|||
});
|
||||
|
||||
it('should request Tines stories', async () => {
|
||||
await connector.getStories();
|
||||
await connector.getStories(undefined, connectorUsageCollector);
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(storiesGetRequestExpected);
|
||||
});
|
||||
|
||||
it('should return the Tines stories reduced array', async () => {
|
||||
const { stories } = await connector.getStories();
|
||||
const { stories } = await connector.getStories(undefined, connectorUsageCollector);
|
||||
expect(stories).toEqual([storyResult]);
|
||||
});
|
||||
|
||||
|
@ -127,7 +137,7 @@ describe('TinesConnector', () => {
|
|||
mockRequest.mockReturnValueOnce({
|
||||
data: { stories: [story], meta: { pages: 1 } },
|
||||
});
|
||||
const response = await connector.getStories();
|
||||
const response = await connector.getStories(undefined, connectorUsageCollector);
|
||||
expect(response.incompleteResponse).toEqual(false);
|
||||
});
|
||||
|
||||
|
@ -135,7 +145,7 @@ describe('TinesConnector', () => {
|
|||
mockRequest.mockReturnValueOnce({
|
||||
data: { stories: [story], meta: { pages: 2 } },
|
||||
});
|
||||
const response = await connector.getStories();
|
||||
const response = await connector.getStories(undefined, connectorUsageCollector);
|
||||
expect(response.incompleteResponse).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
@ -146,14 +156,17 @@ describe('TinesConnector', () => {
|
|||
});
|
||||
|
||||
it('should request Tines webhook actions', async () => {
|
||||
await connector.getWebhooks({ storyId: story.id });
|
||||
await connector.getWebhooks({ storyId: story.id }, connectorUsageCollector);
|
||||
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith(agentsGetRequestExpected);
|
||||
});
|
||||
|
||||
it('should return the Tines webhooks reduced array', async () => {
|
||||
const { webhooks } = await connector.getWebhooks({ storyId: story.id });
|
||||
const { webhooks } = await connector.getWebhooks(
|
||||
{ storyId: story.id },
|
||||
connectorUsageCollector
|
||||
);
|
||||
expect(webhooks).toEqual([webhookResult]);
|
||||
});
|
||||
|
||||
|
@ -161,7 +174,7 @@ describe('TinesConnector', () => {
|
|||
mockRequest.mockReturnValueOnce({
|
||||
data: { agents: [webhookAgent], meta: { pages: 1 } },
|
||||
});
|
||||
const response = await connector.getWebhooks({ storyId: story.id });
|
||||
const response = await connector.getWebhooks({ storyId: story.id }, connectorUsageCollector);
|
||||
expect(response.incompleteResponse).toEqual(false);
|
||||
});
|
||||
|
||||
|
@ -169,7 +182,7 @@ describe('TinesConnector', () => {
|
|||
mockRequest.mockReturnValueOnce({
|
||||
data: { agents: [webhookAgent], meta: { pages: 2 } },
|
||||
});
|
||||
const response = await connector.getWebhooks({ storyId: story.id });
|
||||
const response = await connector.getWebhooks({ storyId: story.id }, connectorUsageCollector);
|
||||
expect(response.incompleteResponse).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
@ -180,10 +193,13 @@ describe('TinesConnector', () => {
|
|||
});
|
||||
|
||||
it('should send data to Tines webhook using selected webhook parameter', async () => {
|
||||
await connector.runWebhook({
|
||||
webhook: webhookResult,
|
||||
body: '[]',
|
||||
});
|
||||
await connector.runWebhook(
|
||||
{
|
||||
webhook: webhookResult,
|
||||
body: '[]',
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
|
@ -194,14 +210,18 @@ describe('TinesConnector', () => {
|
|||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send data to Tines webhook using webhook url parameter', async () => {
|
||||
await connector.runWebhook({
|
||||
webhookUrl,
|
||||
body: '[]',
|
||||
});
|
||||
await connector.runWebhook(
|
||||
{
|
||||
webhookUrl,
|
||||
body: '[]',
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
|
@ -212,6 +232,7 @@ describe('TinesConnector', () => {
|
|||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
connectorUsageCollector,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import {
|
||||
|
@ -108,12 +109,16 @@ export class TinesConnector extends SubActionConnector<TinesConfig, TinesSecrets
|
|||
|
||||
private async tinesApiRequest<R extends TinesBaseApiResponse, T>(
|
||||
req: SubActionRequestParams<R>,
|
||||
reducer: (response: R) => T
|
||||
reducer: (response: R) => T,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<T & { incompleteResponse: boolean }> {
|
||||
const response = await this.request<R>({
|
||||
...req,
|
||||
params: { ...req.params, per_page: API_MAX_RESULTS },
|
||||
});
|
||||
const response = await this.request<R>(
|
||||
{
|
||||
...req,
|
||||
params: { ...req.params, per_page: API_MAX_RESULTS },
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
return {
|
||||
...reducer(response.data),
|
||||
incompleteResponse: response.data.meta.pages > 1,
|
||||
|
@ -130,20 +135,25 @@ export class TinesConnector extends SubActionConnector<TinesConfig, TinesSecrets
|
|||
return `API Error: ${error.response?.statusText}`;
|
||||
}
|
||||
|
||||
public async getStories(): Promise<TinesStoriesActionResponse> {
|
||||
public async getStories(
|
||||
params: unknown,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<TinesStoriesActionResponse> {
|
||||
return this.tinesApiRequest(
|
||||
{
|
||||
url: this.urls.stories,
|
||||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TinesStoriesApiResponseSchema,
|
||||
},
|
||||
storiesReducer
|
||||
storiesReducer,
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async getWebhooks({
|
||||
storyId,
|
||||
}: TinesWebhooksActionParams): Promise<TinesWebhooksActionResponse> {
|
||||
public async getWebhooks(
|
||||
{ storyId }: TinesWebhooksActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<TinesWebhooksActionResponse> {
|
||||
return this.tinesApiRequest(
|
||||
{
|
||||
url: this.urls.agents,
|
||||
|
@ -151,24 +161,27 @@ export class TinesConnector extends SubActionConnector<TinesConfig, TinesSecrets
|
|||
headers: this.getAuthHeaders(),
|
||||
responseSchema: TinesWebhooksApiResponseSchema,
|
||||
},
|
||||
webhooksReducer
|
||||
webhooksReducer,
|
||||
connectorUsageCollector
|
||||
);
|
||||
}
|
||||
|
||||
public async runWebhook({
|
||||
webhook,
|
||||
webhookUrl,
|
||||
body,
|
||||
}: TinesRunActionParams): Promise<TinesRunActionResponse> {
|
||||
public async runWebhook(
|
||||
{ webhook, webhookUrl, body }: TinesRunActionParams,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<TinesRunActionResponse> {
|
||||
if (!webhook && !webhookUrl) {
|
||||
throw Error('Invalid subActionsParams: [webhook] or [webhookUrl] expected but got none');
|
||||
}
|
||||
const response = await this.request({
|
||||
url: webhookUrl ? webhookUrl : this.urls.getRunWebhookURL(webhook!),
|
||||
method: 'post',
|
||||
responseSchema: TinesRunApiResponseSchema,
|
||||
data: body,
|
||||
});
|
||||
const response = await this.request(
|
||||
{
|
||||
url: webhookUrl ? webhookUrl : this.urls.getRunWebhookURL(webhook!),
|
||||
method: 'post',
|
||||
responseSchema: TinesRunApiResponseSchema,
|
||||
data: body,
|
||||
},
|
||||
connectorUsageCollector
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
|
48
x-pack/plugins/stack_connectors/server/connector_types/torq/__snapshots__/index.test.ts.snap
generated
Normal file
48
x-pack/plugins/stack_connectors/server/connector_types/torq/__snapshots__/index.test.ts.snap
generated
Normal file
|
@ -0,0 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`execute Torq action execute with token happy flow 1`] = `
|
||||
Object {
|
||||
"axios": Any<Function>,
|
||||
"connectorUsageCollector": Object {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from Torq action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"msg": "some data",
|
||||
},
|
||||
"headers": Object {
|
||||
"Content-Type": "application/json",
|
||||
"X-Torq-Token": "1234",
|
||||
},
|
||||
"logger": Any<Object>,
|
||||
"method": "post",
|
||||
"url": "https://hooks.torq.io/v1/test",
|
||||
"validateStatus": Any<Function>,
|
||||
}
|
||||
`;
|
|
@ -12,6 +12,7 @@ import { ActionTypeConfigType, getActionType, TorqActionType } from '.';
|
|||
|
||||
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { validateConfig, validateParams, validateSecrets } from '@kbn/actions-plugin/server/lib';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
|
@ -37,10 +38,15 @@ const services: Services = actionsMock.createServices();
|
|||
let actionType: TorqActionType;
|
||||
const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeAll(() => {
|
||||
actionType = getActionType();
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('actionType', () => {
|
||||
|
@ -171,48 +177,29 @@ describe('execute Torq action', () => {
|
|||
params: { body: '{"msg": "some data"}' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
delete requestMock.mock.calls[0][0].configurationUtilities;
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": [MockFunction],
|
||||
"data": Object {
|
||||
"msg": "some data",
|
||||
expect(requestMock.mock.calls[0][0]).toMatchSnapshot({
|
||||
axios: expect.any(Function),
|
||||
connectorUsageCollector: {
|
||||
usage: {
|
||||
requestBodyBytes: 0,
|
||||
},
|
||||
"headers": Object {
|
||||
"Content-Type": "application/json",
|
||||
"X-Torq-Token": "1234",
|
||||
},
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from Torq action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"method": "post",
|
||||
"url": "https://hooks.torq.io/v1/test",
|
||||
"validateStatus": [Function],
|
||||
}
|
||||
`);
|
||||
},
|
||||
data: {
|
||||
msg: 'some data',
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Torq-Token': '1234',
|
||||
},
|
||||
logger: expect.any(Object),
|
||||
method: 'post',
|
||||
url: 'https://hooks.torq.io/v1/test',
|
||||
validateStatus: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('renders parameter templates as expected', async () => {
|
||||
|
|
|
@ -146,6 +146,7 @@ export async function executor(
|
|||
const { webhookIntegrationUrl } = execOptions.config;
|
||||
const { body: data } = execOptions.params;
|
||||
const configurationUtilities = execOptions.configurationUtilities;
|
||||
const connectorUsageCollector = execOptions.connectorUsageCollector;
|
||||
|
||||
const secrets: ActionTypeSecretsType = execOptions.secrets;
|
||||
const token = secrets.token;
|
||||
|
@ -171,6 +172,7 @@ export async function executor(
|
|||
configurationUtilities,
|
||||
logger: execOptions.logger,
|
||||
validateStatus: (status: number) => status >= 200 && status < 300,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
46
x-pack/plugins/stack_connectors/server/connector_types/webhook/__snapshots__/index.test.ts.snap
generated
Normal file
46
x-pack/plugins/stack_connectors/server/connector_types/webhook/__snapshots__/index.test.ts.snap
generated
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`execute() execute with username/password sends request with basic auth 1`] = `
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"connectorUsageCollector": Object {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from webhook action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": "some data",
|
||||
"headers": Object {
|
||||
"Authorization": "Basic YWJjOjEyMw==",
|
||||
"aheader": "a value",
|
||||
},
|
||||
"logger": Any<Object>,
|
||||
"method": "post",
|
||||
"sslOverrides": Object {},
|
||||
"url": "https://abc.def/my-webhook",
|
||||
}
|
||||
`;
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, Services } from '@kbn/actions-plugin/server/types';
|
||||
import { validateConfig, validateParams, validateSecrets } from '@kbn/actions-plugin/server/lib';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
|
@ -41,10 +41,15 @@ const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
|||
|
||||
let connectorType: WebhookConnectorType;
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectorType', () => {
|
||||
|
@ -339,46 +344,27 @@ describe('execute()', () => {
|
|||
params: { body: 'some data' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
delete requestMock.mock.calls[0][0].configurationUtilities;
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"data": "some data",
|
||||
"headers": Object {
|
||||
"Authorization": "Basic YWJjOjEyMw==",
|
||||
"aheader": "a value",
|
||||
expect(requestMock.mock.calls[0][0]).toMatchSnapshot({
|
||||
axios: undefined,
|
||||
connectorUsageCollector: {
|
||||
usage: {
|
||||
requestBodyBytes: 0,
|
||||
},
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from webhook action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"method": "post",
|
||||
"sslOverrides": Object {},
|
||||
"url": "https://abc.def/my-webhook",
|
||||
}
|
||||
`);
|
||||
},
|
||||
data: 'some data',
|
||||
headers: {
|
||||
Authorization: 'Basic YWJjOjEyMw==',
|
||||
aheader: 'a value',
|
||||
},
|
||||
logger: expect.any(Object),
|
||||
method: 'post',
|
||||
sslOverrides: {},
|
||||
url: 'https://abc.def/my-webhook',
|
||||
});
|
||||
});
|
||||
|
||||
test('execute with ssl adds ssl settings to sslOverrides', async () => {
|
||||
|
@ -400,6 +386,7 @@ describe('execute()', () => {
|
|||
params: { body: 'some data' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
delete requestMock.mock.calls[0][0].configurationUtilities;
|
||||
|
@ -407,6 +394,36 @@ describe('execute()', () => {
|
|||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from webhook action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": "some data",
|
||||
"headers": Object {
|
||||
"aheader": "a value",
|
||||
|
@ -588,6 +605,7 @@ describe('execute()', () => {
|
|||
params: { body: 'some data' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(mockedLogger.error).toBeCalledWith(
|
||||
'error on some-id webhook event: maxContentLength size of 1000000 exceeded'
|
||||
|
@ -618,12 +636,43 @@ describe('execute()', () => {
|
|||
params: { body: 'some data' },
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
delete requestMock.mock.calls[0][0].configurationUtilities;
|
||||
expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"axios": undefined,
|
||||
"connectorUsageCollector": ConnectorUsageCollector {
|
||||
"connectorId": "test-connector-id",
|
||||
"logger": Object {
|
||||
"context": Array [],
|
||||
"debug": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"response from webhook action \\"some-id\\": [HTTP 200] ",
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"error": [MockFunction],
|
||||
"fatal": [MockFunction],
|
||||
"get": [MockFunction],
|
||||
"info": [MockFunction],
|
||||
"isLevelEnabled": [MockFunction],
|
||||
"log": [MockFunction],
|
||||
"trace": [MockFunction],
|
||||
"warn": [MockFunction],
|
||||
},
|
||||
"usage": Object {
|
||||
"requestBodyBytes": 0,
|
||||
},
|
||||
},
|
||||
"data": "some data",
|
||||
"headers": Object {
|
||||
"aheader": "a value",
|
||||
|
|
|
@ -128,7 +128,8 @@ function validateConnectorTypeConfig(
|
|||
export async function executor(
|
||||
execOptions: WebhookConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> {
|
||||
const { actionId, config, params, configurationUtilities, logger } = execOptions;
|
||||
const { actionId, config, params, configurationUtilities, logger, connectorUsageCollector } =
|
||||
execOptions;
|
||||
const { method, url, headers = {}, hasAuth, authType, ca, verificationMode } = config;
|
||||
const { body: data } = params;
|
||||
|
||||
|
@ -159,6 +160,7 @@ export async function executor(
|
|||
data,
|
||||
configurationUtilities,
|
||||
sslOverrides,
|
||||
connectorUsageCollector,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
XmattersConnectorType,
|
||||
} from '.';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { Services } from '@kbn/actions-plugin/server/types';
|
||||
import { ConnectorUsageCollector, Services } from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
validateConfig,
|
||||
validateConnector,
|
||||
|
@ -45,10 +45,15 @@ const mockedLogger: jest.Mocked<Logger> = loggerMock.create();
|
|||
|
||||
let connectorType: XmattersConnectorType;
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
let connectorUsageCollector: ConnectorUsageCollector;
|
||||
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
connectorUsageCollector = new ConnectorUsageCollector({
|
||||
logger: mockedLogger,
|
||||
connectorId: 'test-connector-id',
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectorType', () => {
|
||||
|
@ -423,6 +428,7 @@ describe('execute()', () => {
|
|||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const { method, url, headers, data } = requestMock.mock.calls[0][0];
|
||||
|
@ -472,6 +478,7 @@ describe('execute()', () => {
|
|||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
expect(mockedLogger.warn).toBeCalledWith(
|
||||
'Error thrown triggering xMatters workflow: maxContentLength size of 1000000 exceeded'
|
||||
|
@ -504,6 +511,7 @@ describe('execute()', () => {
|
|||
},
|
||||
configurationUtilities,
|
||||
logger: mockedLogger,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
|
||||
const { method, url, headers, data } = requestMock.mock.calls[0][0];
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { isString } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import type {
|
||||
import {
|
||||
ActionType as ConnectorType,
|
||||
ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
|
||||
ActionTypeExecutorResult as ConnectorTypeExecutorResult,
|
||||
|
@ -247,7 +247,8 @@ function validateConnectorTypeSecrets(
|
|||
export async function executor(
|
||||
execOptions: XmattersConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<unknown>> {
|
||||
const { actionId, configurationUtilities, config, params, logger } = execOptions;
|
||||
const { actionId, configurationUtilities, config, params, logger, connectorUsageCollector } =
|
||||
execOptions;
|
||||
const { configUrl, usesBasic } = config;
|
||||
const data = getPayloadForRequest(params);
|
||||
|
||||
|
@ -263,7 +264,12 @@ export async function executor(
|
|||
if (!url) {
|
||||
throw new Error('Error: no url provided');
|
||||
}
|
||||
result = await postXmatters({ url, data, basicAuth }, logger, configurationUtilities);
|
||||
result = await postXmatters(
|
||||
{ url, data, basicAuth },
|
||||
logger,
|
||||
configurationUtilities,
|
||||
connectorUsageCollector
|
||||
);
|
||||
} catch (err) {
|
||||
const message = i18n.translate('xpack.stackConnectors.xmatters.postingErrorMessage', {
|
||||
defaultMessage: 'Error triggering xMatters workflow',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Logger } from '@kbn/core/server';
|
|||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
|
||||
import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
interface PostXmattersOptions {
|
||||
url: string;
|
||||
|
@ -34,7 +35,8 @@ interface PostXmattersOptions {
|
|||
export async function postXmatters(
|
||||
options: PostXmattersOptions,
|
||||
logger: Logger,
|
||||
configurationUtilities: ActionsConfigurationUtilities
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
): Promise<AxiosResponse> {
|
||||
const { url, data, basicAuth } = options;
|
||||
const axiosInstance = axios.create();
|
||||
|
@ -50,5 +52,6 @@ export async function postXmatters(
|
|||
data,
|
||||
configurationUtilities,
|
||||
validateStatus: () => true,
|
||||
connectorUsageCollector,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { ServiceParams } from '@kbn/actions-plugin/server';
|
|||
import { PluginSetupContract as ActionsPluginSetup } from '@kbn/actions-plugin/server/plugin';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { ConnectorUsageCollector } from '@kbn/actions-plugin/server/types';
|
||||
|
||||
const TestConfigSchema = schema.object({ url: schema.string() });
|
||||
const TestSecretsSchema = schema.object({
|
||||
|
@ -69,7 +70,11 @@ export const getTestSubActionConnector = (
|
|||
return `Message: ${error.response?.data.errorMessage}. Code: ${error.response?.data.errorCode}`;
|
||||
}
|
||||
|
||||
public async subActionWithParams({ id }: { id: string }) {
|
||||
public async subActionWithParams(
|
||||
{ id }: { id: string },
|
||||
connectorUsageCollector: ConnectorUsageCollector
|
||||
) {
|
||||
connectorUsageCollector.addRequestBodyBytes(undefined, { id });
|
||||
return { id };
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,9 @@ import {
|
|||
} from '@kbn/actions-simulators-plugin/server/bedrock_simulation';
|
||||
import { DEFAULT_TOKEN_LIMIT } from '@kbn/stack-connectors-plugin/common/bedrock/constants';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/generated/schemas';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
import { getEventLog, getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
|
||||
const connectorTypeId = '.bedrock';
|
||||
const name = 'A bedrock action';
|
||||
|
@ -344,6 +345,23 @@ export default function bedrockTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: bedrockActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(145);
|
||||
});
|
||||
|
||||
it('should overwrite the model when a model argument is provided', async () => {
|
||||
|
@ -374,6 +392,23 @@ export default function bedrockTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: bedrockActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { gte: 2 }],
|
||||
['execute', { gte: 2 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[3];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(145);
|
||||
});
|
||||
|
||||
it('should invoke AI with assistant AI body argument formatted to bedrock expectations', async () => {
|
||||
|
@ -423,6 +458,23 @@ export default function bedrockTest({ getService }: FtrProviderContext) {
|
|||
connector_id: bedrockActionId,
|
||||
data: { message: bedrockClaude2SuccessResponse.completion },
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: bedrockActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { gte: 3 }],
|
||||
['execute', { gte: 3 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[5];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(256);
|
||||
});
|
||||
|
||||
describe('Token tracking dashboard', () => {
|
||||
|
|
|
@ -14,13 +14,16 @@ import {
|
|||
ExternalServiceSimulator,
|
||||
} from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/generated/schemas';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getEventLog } from '../../../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const configService = getService('config');
|
||||
const retry = getService('retry');
|
||||
const config = {
|
||||
createCommentJson: '{"body":{{{case.comment}}}}',
|
||||
createCommentMethod: 'post',
|
||||
|
@ -398,6 +401,23 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
|
|||
const { pushedDate, ...dataWithoutTime } = body.data;
|
||||
body.data = dataWithoutTime;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: simulatedActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(125);
|
||||
|
||||
expect(body).to.eql({
|
||||
status: 'ok',
|
||||
connector_id: simulatedActionId,
|
||||
|
|
|
@ -12,7 +12,9 @@ import {
|
|||
d3SecuritySuccessResponse,
|
||||
} from '@kbn/actions-simulators-plugin/server/d3security_simulation';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/generated/schemas';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getEventLog } from '../../../../../common/lib';
|
||||
|
||||
const connectorTypeId = '.d3security';
|
||||
const name = 'A D3 Security action';
|
||||
|
@ -24,6 +26,7 @@ const secrets = {
|
|||
export default function d3SecurityTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const configService = getService('config');
|
||||
const retry = getService('retry');
|
||||
|
||||
const createConnector = async (url: string) => {
|
||||
const { body } = await supertest
|
||||
|
@ -248,6 +251,24 @@ export default function d3SecurityTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: d3SecurityActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(99);
|
||||
|
||||
expect(body).to.eql({
|
||||
status: 'ok',
|
||||
connector_id: d3SecurityActionId,
|
||||
|
|
|
@ -11,12 +11,15 @@ import {
|
|||
ExternalServiceSimulator,
|
||||
getExternalServiceSimulatorPath,
|
||||
} from '@kbn/actions-simulators-plugin/server/plugin';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/generated/schemas';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getEventLog } from '../../../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function emailTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('create email action', () => {
|
||||
let createdActionId = '';
|
||||
|
@ -103,7 +106,7 @@ export default function emailTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
.then(async (resp: any) => {
|
||||
expect(resp.body.data.message.messageId).to.be.a('string');
|
||||
expect(resp.body.data.messageId).to.be.a('string');
|
||||
|
||||
|
@ -131,6 +134,23 @@ export default function emailTest({ getService }: FtrProviderContext) {
|
|||
headers: {},
|
||||
},
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: createdActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(350);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
import type { Client } from '@elastic/elasticsearch';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/generated/schemas';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getEventLog } from '../../../../../common/lib';
|
||||
|
||||
const ES_TEST_INDEX_NAME = 'functional-test-actions-index';
|
||||
|
||||
|
@ -16,6 +18,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
|
|||
const es: Client = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('index action', () => {
|
||||
beforeEach(() => esDeleteAllIndices(ES_TEST_INDEX_NAME));
|
||||
|
@ -214,6 +217,23 @@ export default function indexTest({ getService }: FtrProviderContext) {
|
|||
}
|
||||
expect(passed1).to.be(true);
|
||||
expect(passed2).to.be(true);
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: createdAction.id,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(0);
|
||||
});
|
||||
|
||||
it('should execute successly with refresh false', async () => {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import httpProxy from 'http-proxy';
|
||||
import expect from '@kbn/expect';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/generated/schemas';
|
||||
|
||||
import { getHttpProxyServer } from '@kbn/alerting-api-integration-helpers';
|
||||
import {
|
||||
|
@ -16,12 +17,14 @@ import {
|
|||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { MAX_OTHER_FIELDS_LENGTH } from '@kbn/stack-connectors-plugin/common/jira/constants';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getEventLog } from '../../../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function jiraTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const configService = getService('config');
|
||||
const retry = getService('retry');
|
||||
|
||||
const mockJira = {
|
||||
config: {
|
||||
|
@ -517,6 +520,23 @@ export default function jiraTest({ getService }: FtrProviderContext) {
|
|||
url: `${jiraSimulatorURL}/browse/CK-1`,
|
||||
},
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: simulatedActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { gte: 2 }],
|
||||
['execute', { gte: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(124);
|
||||
});
|
||||
|
||||
it('should handle creating an incident with other fields', async () => {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/server';
|
||||
|
||||
import {
|
||||
OpenAISimulator,
|
||||
|
@ -14,6 +15,7 @@ import {
|
|||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
import { getEventLog } from '../../../../../common/lib';
|
||||
|
||||
const connectorTypeId = '.gen-ai';
|
||||
const name = 'A genAi action';
|
||||
|
@ -315,6 +317,23 @@ export default function genAiTest({ getService }: FtrProviderContext) {
|
|||
connector_id: genAiActionId,
|
||||
data: genAiSuccessResponse,
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: genAiActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(78);
|
||||
});
|
||||
describe('Token tracking dashboard', () => {
|
||||
const dashboardId = 'specific-dashboard-id-default';
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { IValidatedEvent } from '@kbn/event-log-plugin/server';
|
||||
|
||||
import {
|
||||
OpsgenieSimulator,
|
||||
|
@ -13,11 +14,13 @@ import {
|
|||
} from '@kbn/actions-simulators-plugin/server/opsgenie_simulation';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { getEventLog } from '../../../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function opsgenieTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const configService = getService('config');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('Opsgenie', () => {
|
||||
describe('action creation', () => {
|
||||
|
@ -535,6 +538,23 @@ export default function opsgenieTest({ getService }: FtrProviderContext) {
|
|||
connector_id: opsgenieActionId,
|
||||
data: opsgenieSuccessResponse,
|
||||
});
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: 'default',
|
||||
type: 'action',
|
||||
id: opsgenieActionId,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.action?.execution?.usage?.request_body_bytes).to.be(21);
|
||||
});
|
||||
|
||||
it('should preserve the alias when it is 512 characters when creating an alert', async () => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue