Skip to content

Commit

Permalink
tests for upload action handler component
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-tavares committed May 9, 2023
1 parent f5a0bae commit 955c706
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ = Object.freeze<
release: 'canUnIsolateHost',
execute: 'canWriteFileOperations',
'get-file': 'canWriteFileOperations',
upload: 'canWriteFileOperations',
processes: 'canGetRunningProcesses',
'kill-process': 'canKillProcess',
'suspend-process': 'canSuspendProcess',
upload: 'canWriteExecuteOperations',
});

// 4 hrs in seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,9 @@ interface ConsoleSelectorsAndActionsMock {
getInputText: () => string;
openHelpPanel: () => void;
closeHelpPanel: () => void;
}

export interface ConsoleTestSetup
extends Pick<
AppContextTestRender,
'startServices' | 'coreStart' | 'depsStart' | 'queryClient' | 'history' | 'setExperimentalFlag'
> {
renderConsole(props?: Partial<ConsoleProps>): ReturnType<AppContextTestRender['render']>;

commands: CommandDefinition[];

/** Clicks on the submit button on the far right of the console's input area */
submitCommand: () => void;
/** Enters a command into the console's input area */
enterCommand(
cmd: string,
options?: Partial<{
Expand All @@ -52,6 +44,18 @@ export interface ConsoleTestSetup
useKeyboard: boolean;
}>
): void;
}

export interface ConsoleTestSetup
extends Pick<
AppContextTestRender,
'startServices' | 'coreStart' | 'depsStart' | 'queryClient' | 'history' | 'setExperimentalFlag'
> {
renderConsole(props?: Partial<ConsoleProps>): ReturnType<AppContextTestRender['render']>;

commands: CommandDefinition[];

enterCommand: ConsoleSelectorsAndActionsMock['enterCommand'];

selectors: ConsoleSelectorsAndActionsMock;
}
Expand Down Expand Up @@ -90,13 +94,21 @@ export const getConsoleSelectorsAndActionMock = (
renderResult.getByTestId(`${dataTestSubj}-sidePanel-headerCloseButton`).click();
}
};
const submitCommand: ConsoleSelectorsAndActionsMock['submitCommand'] = () => {
renderResult.getByTestId(`${dataTestSubj}-inputTextSubmitButton`).click();
};
const enterCommand: ConsoleSelectorsAndActionsMock['enterCommand'] = (cmd, options = {}) => {
enterConsoleCommand(renderResult, cmd, options);
};

return {
getInputText,
getLeftOfCursorInputText,
getRightOfCursorInputText,
openHelpPanel,
closeHelpPanel,
submitCommand,
enterCommand,
};
};

Expand Down Expand Up @@ -206,6 +218,17 @@ export const getConsoleTestSetup = (): ConsoleTestSetup => {
initSelectorsIfNeeded();
return selectors.closeHelpPanel();
},
submitCommand: () => {
initSelectorsIfNeeded();
return selectors.submitCommand();
},
enterCommand: (
cmd: string,
options?: Partial<{ inputOnly: boolean; useKeyboard: boolean }>
) => {
initSelectorsIfNeeded();
return selectors.enterCommand(cmd, options);
},
},
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const ArgumentFileSelector = memo<
onChange={handleFileSelection}
fullWidth
display="large"
data-test-subj="console-arg-file-picker"
/>
)}
</EuiPopover>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* 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 {
type EndpointCapabilities,
ENDPOINT_CAPABILITIES,
} from '../../../../../../common/endpoint/service/response_actions/constants';
import {
type AppContextTestRender,
createAppRootMockRenderer,
} from '../../../../../common/mock/endpoint';
import { responseActionsHttpMocks } from '../../../../mocks/response_actions_http_mocks';
import {
ConsoleManagerTestComponent,
getConsoleManagerMockRenderResultQueriesAndActions,
} from '../../../console/components/console_manager/mocks';
import type { EndpointPrivileges } from '../../../../../../common/endpoint/types';
import { getEndpointAuthzInitialStateMock } from '../../../../../../common/endpoint/service/authz/mocks';
import { getEndpointConsoleCommands } from '../..';
import React from 'react';
import { getConsoleSelectorsAndActionMock } from '../../../console/mocks';
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { executionTranslations } from '../../../console/components/console_state/state_update_handlers/translations';
import { UPLOAD_ROUTE } from '../../../../../../common/endpoint/constants';
import type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser';
import {
INSUFFICIENT_PRIVILEGES_FOR_COMMAND,
UPGRADE_ENDPOINT_FOR_RESPONDER,
} from '../../../../../common/translations';

describe('When using `upload` response action', () => {
let render: (
capabilities?: EndpointCapabilities[]
) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;
let consoleManagerMockAccess: ReturnType<
typeof getConsoleManagerMockRenderResultQueriesAndActions
>;
let endpointPrivileges: EndpointPrivileges;
let endpointCapabilities: typeof ENDPOINT_CAPABILITIES;
let file: File;
let console: ReturnType<typeof getConsoleSelectorsAndActionMock>;

beforeEach(() => {
const mockedContext = createAppRootMockRenderer();

mockedContext.setExperimentalFlag({ responseActionUploadEnabled: true });
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
endpointPrivileges = { ...getEndpointAuthzInitialStateMock(), loading: false };
endpointCapabilities = [...ENDPOINT_CAPABILITIES];

const fileContent = new Blob(['test']);
file = new File([fileContent], 'test.json', { type: 'application/JSON' });

render = async () => {
renderResult = mockedContext.render(
<ConsoleManagerTestComponent
registerConsoleProps={() => {
return {
consoleProps: {
'data-test-subj': 'test',
commands: getEndpointConsoleCommands({
endpointAgentId: 'a.b.c',
endpointCapabilities,
endpointPrivileges,
}),
},
};
}}
/>
);

console = getConsoleSelectorsAndActionMock(renderResult);
consoleManagerMockAccess = getConsoleManagerMockRenderResultQueriesAndActions(renderResult);

await consoleManagerMockAccess.clickOnRegisterNewConsole();
await consoleManagerMockAccess.openRunningConsole();

return renderResult;
};
});

afterEach(() => {
// @ts-expect-error assignment of `undefined` to avoid leak from one test to the other
console = undefined;
// @ts-expect-error assignment of `undefined` to avoid leak from one test to the other
consoleManagerMockAccess = undefined;
});

it('should require `--file` argument', async () => {
await render();
console.enterCommand('upload');

await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument-message')).toHaveTextContent(
executionTranslations.missingArguments('--file')
);
});
});

it('should error if `--file` argument is not set (no file selected)', async () => {
await render();
console.enterCommand('upload --file');

await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument-message')).toHaveTextContent(
executionTranslations.mustHaveValue('file')
);
});
});

it('should call upload api with expected payload', async () => {
const { getByTestId } = await render();
console.enterCommand('upload --file', { inputOnly: true });

await waitFor(() => {
userEvent.upload(getByTestId('console-arg-file-picker'), file);
});

console.submitCommand();

await waitFor(() => {
expect(apiMocks.responseProvider.upload).toHaveBeenCalledWith({
body: expect.any(FormData),
headers: {
'Content-Type': undefined,
},
path: UPLOAD_ROUTE,
});
});

const apiBody = (
apiMocks.responseProvider.upload.mock.lastCall as unknown as [HttpFetchOptionsWithPath]
)?.[0].body as FormData;

expect(apiBody.get('file')).toEqual(file);
expect(apiBody.get('endpoint_ids')).toEqual('["a.b.c"]');
expect(apiBody.get('parameters')).toEqual('{}');
});

it('should allow `--overwrite` argument', async () => {
const { getByTestId } = await render();
console.enterCommand('upload --overwrite --file', { inputOnly: true });

await waitFor(() => {
userEvent.upload(getByTestId('console-arg-file-picker'), file);
});

console.submitCommand();

await waitFor(() => {
expect(apiMocks.responseProvider.upload).toHaveBeenCalled();
});

const apiBody = (
apiMocks.responseProvider.upload.mock.lastCall as unknown as [HttpFetchOptionsWithPath]
)?.[0].body as FormData;

expect(apiBody.get('parameters')).toEqual('{"overwrite":true}');
});

it('should show an error if user has no authz to file operations', async () => {
endpointPrivileges.canWriteFileOperations = false;
const { getByTestId } = await render();
console.enterCommand('upload --overwrite --file', { inputOnly: true });

await waitFor(() => {
userEvent.upload(getByTestId('console-arg-file-picker'), file);
});

console.submitCommand();

await waitFor(() => {
expect(getByTestId('test-validationError-message').textContent).toEqual(
INSUFFICIENT_PRIVILEGES_FOR_COMMAND
);
});
});

it('should show an error if the endpoint does not support `upload_file` capability', async () => {
endpointCapabilities = [] as unknown as typeof ENDPOINT_CAPABILITIES;
const { getByTestId } = await render();
console.enterCommand('upload --overwrite --file', { inputOnly: true });

await waitFor(() => {
userEvent.upload(getByTestId('console-arg-file-picker'), file);
});

console.submitCommand();

await waitFor(() => {
expect(getByTestId('test-validationError-message').textContent).toEqual(
UPGRADE_ENDPOINT_FOR_RESPONDER
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GET_FILE_ROUTE,
ACTION_AGENT_FILE_INFO_ROUTE,
EXECUTE_ROUTE,
UPLOAD_ROUTE,
} from '../../../common/endpoint/constants';
import type { ResponseProvidersInterface } from '../../common/mock/endpoint/http_handler_mock_factory';
import { httpHandlerMockFactory } from '../../common/mock/endpoint/http_handler_mock_factory';
Expand All @@ -34,6 +35,8 @@ import type {
ActionFileInfoApiResponse,
ResponseActionExecuteOutputContent,
ResponseActionsExecuteParameters,
ResponseActionUploadOutputContent,
ResponseActionUploadParameters,
} from '../../../common/endpoint/types';

export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{
Expand All @@ -58,6 +61,11 @@ export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{
fileInfo: () => ActionFileInfoApiResponse;

execute: () => ActionDetailsApiResponse<ResponseActionExecuteOutputContent>;

upload: () => ActionDetailsApiResponse<
ResponseActionUploadOutputContent,
ResponseActionUploadParameters
>;
}>;

export const responseActionsHttpMocks = httpHandlerMockFactory<ResponseActionsHttpMocksInterface>([
Expand Down Expand Up @@ -218,6 +226,25 @@ export const responseActionsHttpMocks = httpHandlerMockFactory<ResponseActionsHt
},
});

return { data: response };
},
},
{
id: 'upload',
path: UPLOAD_ROUTE,
method: 'post',
handler: (): ActionDetailsApiResponse<
ResponseActionUploadOutputContent,
ResponseActionUploadParameters
> => {
const generator = new EndpointActionGenerator('seed');
const response = generator.generateActionDetails<
ResponseActionUploadOutputContent,
ResponseActionUploadParameters
>({
command: 'upload',
});

return { data: response };
},
},
Expand Down

0 comments on commit 955c706

Please sign in to comment.