[Automatic Import] Adding UI and FTR tests for automatic import cel creation flyout (#209418)

## Summary

This PR adds the following tests for Automatic Import:
- jest unit tests for the CEL generation flyout
- FTR tests for the `analyze_api` and `cel` graph endpoints (excluding
200 tests due to https://github.com/elastic/kibana/issues/204177 still
being open)

There is also some very minor cleanup of a test mocking of the now
deprecated FF for generateCel, and small refactor to move a function to
a different file for consistency.

(Cypress tests coming in a separate PR)
This commit is contained in:
Kylie Meli 2025-02-06 09:29:56 -05:00 committed by GitHub
parent f058b50f93
commit 5878c77784
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1059 additions and 23 deletions

View file

@ -0,0 +1,53 @@
/*
* 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 React from 'react';
import { render, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../../mocks/test_provider';
import { ActionsProvider } from '../../state';
import { mockActions, mockState } from '../../mocks/state';
import { CreateCelConfigFlyout } from './create_cel_config';
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
</TestProvider>
);
describe('CreateCelConfig', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<CreateCelConfigFlyout
integrationSettings={undefined}
connector={mockState.connector}
isFlyoutGenerating={false}
/>,
{ wrapper }
);
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('when open with initial state', () => {
it('should render upload spec step', () => {
expect(result.queryByTestId('uploadSpecStep')).toBeVisible();
});
it('confirm settings step collapsed', () => {
const accordionButton = result.queryByTestId('celGenStep2')?.querySelector('button');
// Check the aria-expanded property of the button
const isExpanded = accordionButton?.getAttribute('aria-expanded');
expect(isExpanded).toBe('false');
expect(result.queryByTestId('confirmSettingsStep')).toBeNull();
});
});
});

View file

@ -138,6 +138,7 @@ export const CreateCelConfigFlyout = React.memo<CreateCelConfigFlyoutProps>(
paddingSize="m"
forceState={isUploadStepExpanded ? 'open' : 'closed'}
onToggle={handleToggleStep}
data-test-subj="celGenStep1"
buttonContent={
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiStepNumber
@ -174,6 +175,7 @@ export const CreateCelConfigFlyout = React.memo<CreateCelConfigFlyoutProps>(
isDisabled={!isAnalyzeApiGenerationComplete}
forceState={isConfirmStepExpanded ? 'open' : 'closed'}
onToggle={handleToggleStep}
data-test-subj="celGenStep2"
buttonContent={
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiStepNumber

View file

@ -0,0 +1,118 @@
/*
* 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 React from 'react';
import { render, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../../mocks/test_provider';
import { Footer } from './footer';
import { ActionsProvider } from '../../state';
import { mockActions } from '../../mocks/state';
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
</TestProvider>
);
describe('Footer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when rendered everything enabled', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer
isFlyoutGenerating={false}
isValid={false}
isGenerationComplete={false}
showHint={false}
hint={''}
onCancel={() => {}}
onSave={() => {}}
/>,
{
wrapper,
}
);
});
it('should render cancel button', () => {
expect(result.queryByTestId('footer-cancelButton')).toBeInTheDocument();
});
it('should render save button', () => {
expect(result.queryByTestId('footer-saveButton')).toBeInTheDocument();
});
it('should not render hint', () => {
expect(result.queryByTestId('footer-showHint')).not.toBeInTheDocument();
});
});
describe('when rendered with show validation', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer
isFlyoutGenerating={false}
isValid={false}
isGenerationComplete={false}
showHint={true}
hint={''}
onCancel={() => {}}
onSave={() => {}}
/>,
{
wrapper,
}
);
});
it('should render enabled cancel button', () => {
expect(result.queryByTestId('footer-cancelButton')).toBeInTheDocument();
});
it('should render disabled save button', () => {
expect(result.queryByTestId('footer-saveButton')).toBeDisabled();
});
it('should render hint', () => {
expect(result.queryByTestId('footer-showHint')).toBeInTheDocument();
});
});
describe('when rendered while generating', () => {
let result: RenderResult;
beforeEach(() => {
result = render(
<Footer
isFlyoutGenerating={true}
isValid={false}
isGenerationComplete={false}
showHint={false}
hint={''}
onCancel={() => {}}
onSave={() => {}}
/>,
{
wrapper,
}
);
});
it('should render enabled cancel button', () => {
expect(result.queryByTestId('footer-cancelButton')).toBeInTheDocument();
});
it('should render disabled save button', () => {
expect(result.queryByTestId('footer-saveButton')).toBeDisabled();
});
it('should render hint', () => {
expect(result.queryByTestId('footer-showHint')).not.toBeInTheDocument();
});
});
});

View file

@ -30,7 +30,11 @@ export const Footer = React.memo<FooterProps>(
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
{showHint && <EuiText size="s">{hint}</EuiText>}
{showHint && (
<EuiText size="s" data-test-subj="footer-showHint">
{hint}
</EuiText>
)}
<EuiButton
fill={isGenerationComplete}
color="primary"

View file

@ -0,0 +1,24 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { GenerationError } from './generation_error';
describe('GenerationError', () => {
it('should render error', () => {
const title = 'testErrorTitle';
const errorMessage = 'testErrorMessage';
const retryAction = () => {};
const { getByText } = render(
<GenerationError title={title} error={errorMessage} retryAction={retryAction} />
);
const errorElement = getByText('testErrorTitle');
expect(errorElement).toBeInTheDocument();
});
});

View file

@ -0,0 +1,68 @@
/*
* 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 React from 'react';
import { render, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../../../../mocks/test_provider';
import { ActionsProvider } from '../../../../state';
import { mockActions } from '../../../../mocks/state';
import { AuthSelection } from './auth_selection';
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
</TestProvider>
);
describe('AuthSelection', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when open with initial state', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<AuthSelection
selectedAuth={'Basic'}
specifiedAuthForPath={['Basic']}
invalidAuth={false}
isGenerating={false}
showValidation={false}
onChangeAuth={() => {}}
/>,
{ wrapper }
);
});
it('should render auth selection combobox', () => {
expect(result.queryByTestId('authInputComboBox')).toBeInTheDocument();
});
});
describe('invalid auth & showing validation', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<AuthSelection
selectedAuth={'Basic'}
specifiedAuthForPath={['Basic']}
invalidAuth={true}
isGenerating={false}
showValidation={true}
onChangeAuth={() => {}}
/>,
{ wrapper }
);
});
it('should render warning', () => {
expect(result.queryByTestId('authDoesNotAlignWarning')).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,97 @@
/*
* 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 React from 'react';
import { render, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../../../../mocks/test_provider';
import { ActionsProvider } from '../../../../state';
import { mockActions, mockState } from '../../../../mocks/state';
import { ConfirmSettingsStep } from './confirm_settings_step';
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
</TestProvider>
);
jest.mock('@elastic/eui', () => {
return {
...jest.requireActual('@elastic/eui'),
// Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
// which does not produce a valid component wrapper
EuiComboBox: (props: { onChange: (options: unknown) => void; 'data-test-subj': string }) => (
<input
data-test-subj={props['data-test-subj']}
onChange={(syntheticEvent) => {
props.onChange([{ value: syntheticEvent.target.value }]);
}}
/>
),
};
});
describe('ConfirmSettingsStep', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when open with initial state', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<ConfirmSettingsStep
integrationSettings={undefined}
connector={mockState.connector}
isFlyoutGenerating={false}
suggestedPaths={['/path1', '/path2', '/path3']}
showValidation={false}
onShowValidation={() => {}}
onUpdateValidation={() => {}}
onUpdateNeedsGeneration={() => {}}
onCelInputGenerationComplete={() => {}}
/>,
{ wrapper }
);
});
it('should render confirm settings step', () => {
expect(result.queryByTestId('suggestedPathsRadioGroup')).toBeInTheDocument();
expect(result.queryByTestId('authInputComboBox')).toBeInTheDocument();
});
it('generate button enabled', () => {
expect(result.queryByTestId('generateCelInputButton')).toBeEnabled();
});
});
describe('generating in progress', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<ConfirmSettingsStep
integrationSettings={undefined}
connector={mockState.connector}
isFlyoutGenerating={true}
suggestedPaths={['/path1', '/path2', '/path3']}
showValidation={false}
onShowValidation={() => {}}
onUpdateValidation={() => {}}
onUpdateNeedsGeneration={() => {}}
onCelInputGenerationComplete={() => {}}
/>,
{ wrapper }
);
});
it('generate button disabled; cancel button appears and is enabled', () => {
expect(result.queryByTestId('generateCelInputButton')).toBeDisabled();
expect(result.queryByTestId('cancelCelGenerationButton')).toBeVisible();
});
});
});

View file

@ -26,6 +26,7 @@ import { EndpointSelection } from './endpoint_selection';
import { AuthSelection } from './auth_selection';
import { GenerationError } from '../../generation_error';
import { useTelemetry } from '../../../../../telemetry';
import type { IntegrationSettings } from '../../../../types';
export const translateDisplayAuthToType = (auth: string): string => {
return auth === 'API Token' ? 'Header' : auth;
@ -41,6 +42,14 @@ const getSpecifiedAuthForPath = (apiSpec: Oas | undefined, path: string) => {
return specifiedAuth;
};
const loadPaths = (integrationSettings: IntegrationSettings | undefined): string[] => {
const pathObjs = integrationSettings?.apiSpec?.getPaths();
if (!pathObjs) {
return [];
}
return Object.keys(pathObjs).filter((path) => pathObjs[path].get);
};
interface ConfirmSettingsStepProps {
integrationSettings: State['integrationSettings'];
connector: State['connector'];
@ -296,7 +305,7 @@ export const ConfirmSettingsStep = React.memo<ConfirmSettingsStepProps>(
<EuiFlexGroup direction="column" gutterSize="l" data-test-subj="confirmSettingsStep">
<EuiPanel hasShadow={false} hasBorder={false}>
<EndpointSelection
integrationSettings={integrationSettings}
allPaths={loadPaths(integrationSettings)}
pathSuggestions={suggestedPaths}
selectedPath={selectedPath}
selectedOtherPath={selectedOtherPath}
@ -330,7 +339,10 @@ export const ConfirmSettingsStep = React.memo<ConfirmSettingsStepProps>(
<EuiButton
fill
fullWidth={false}
isDisabled={isFlyoutGenerating}
isDisabled={
isFlyoutGenerating ||
(showValidation && (fieldValidationErrors.path || fieldValidationErrors.auth))
}
isLoading={isFlyoutGenerating}
iconSide="right"
color="primary"

View file

@ -0,0 +1,104 @@
/*
* 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 React from 'react';
import { render, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../../../../mocks/test_provider';
import { ActionsProvider } from '../../../../state';
import { mockActions } from '../../../../mocks/state';
import { EndpointSelection } from './endpoint_selection';
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
</TestProvider>
);
describe('EndpointSelection', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when open with initial state', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<EndpointSelection
allPaths={['/path1', '/path2', '/path3', '/path4']}
pathSuggestions={['/path1', '/path2', '/path3']}
selectedPath={'/path1'}
selectedOtherPath={undefined}
useOtherEndpoint={false}
isGenerating={false}
showValidation={false}
onChangeSuggestedPath={() => {}}
onChangeOtherPath={() => {}}
/>,
{ wrapper }
);
});
it('should render endpoint selection radio group', () => {
expect(result.queryByTestId('suggestedPathsRadioGroup')).toBeInTheDocument();
});
it('should render all path options combo box', () => {
expect(result.queryByTestId('allPathOptionsComboBox')).toBeNull();
});
});
describe('show all path options radio group', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<EndpointSelection
allPaths={['/path1', '/path2', '/path3', '/path4']}
pathSuggestions={['/path1', '/path2', '/path3']}
selectedPath={'Enter Manually'}
selectedOtherPath={undefined}
useOtherEndpoint={true}
isGenerating={false}
showValidation={false}
onChangeSuggestedPath={() => {}}
onChangeOtherPath={() => {}}
/>,
{ wrapper }
);
});
it('should render all path options combo box', () => {
expect(result.queryByTestId('allPathOptionsComboBox')).toBeInTheDocument();
});
});
describe('invalid auth & showing validation', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<EndpointSelection
allPaths={['/path1', '/path2', '/path3', '/path4']}
pathSuggestions={['/path1', '/path2', '/path3']}
selectedPath={'Enter manually'}
selectedOtherPath={undefined}
useOtherEndpoint={true}
isGenerating={false}
showValidation={true}
onChangeSuggestedPath={() => {}}
onChangeOtherPath={() => {}}
/>,
{ wrapper }
);
});
it('validation', () => {
const otherEndpointSelection = result.getByTestId('allPathOptionsComboBox');
expect(otherEndpointSelection).toHaveAttribute('aria-invalid');
});
});
});

View file

@ -19,18 +19,9 @@ import {
EuiTitle,
} from '@elastic/eui';
import * as i18n from './translations';
import type { IntegrationSettings } from '../../../../types';
const loadPaths = (integrationSettings: IntegrationSettings | undefined): string[] => {
const pathObjs = integrationSettings?.apiSpec?.getPaths();
if (!pathObjs) {
return [];
}
return Object.keys(pathObjs).filter((path) => pathObjs[path].get);
};
interface EndpointSelectionProps {
integrationSettings: IntegrationSettings | undefined;
allPaths: string[];
pathSuggestions: string[];
selectedPath: string | undefined;
selectedOtherPath: string | undefined;
@ -43,7 +34,7 @@ interface EndpointSelectionProps {
export const EndpointSelection = React.memo<EndpointSelectionProps>(
({
integrationSettings,
allPaths,
pathSuggestions,
selectedPath,
selectedOtherPath,
@ -53,7 +44,6 @@ export const EndpointSelection = React.memo<EndpointSelectionProps>(
onChangeSuggestedPath,
onChangeOtherPath,
}) => {
const allPaths = loadPaths(integrationSettings);
const otherPathOptions = allPaths.map<EuiComboBoxOptionOption>((p) => ({ label: p }));
const hasSuggestedPaths = pathSuggestions.length > 0;
@ -113,6 +103,7 @@ export const EndpointSelection = React.memo<EndpointSelectionProps>(
fullWidth
options={otherPathOptions}
isDisabled={isGenerating}
aria-invalid={showValidation && useOtherEndpoint && selectedOtherPath === undefined}
isInvalid={showValidation && useOtherEndpoint && selectedOtherPath === undefined}
selectedOptions={
selectedOtherPath === undefined ? undefined : [{ label: selectedOtherPath }]

View file

@ -0,0 +1,208 @@
/*
* 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 React from 'react';
import { act, fireEvent, render, waitFor, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../../../../mocks/test_provider';
import { ApiDefinitionInput } from './api_definition_input';
import { ActionsProvider } from '../../../../state';
import { mockActions } from '../../../../mocks/state';
import Oas from 'oas';
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
</TestProvider>
);
const changeFile = async (input: HTMLElement, file: File) => {
await act(async () => {
fireEvent.change(input, { target: { files: [file] } });
await waitFor(() => expect(input).toHaveAttribute('data-loading', 'true'));
await waitFor(() => expect(input).toHaveAttribute('data-loading', 'false'));
});
};
const simpleOpenApiJson = `{"openapi":"3.0.0","info":{"title":"Sample API"},"paths":{"/users":{"get":{"summary":"Returns a list of users.","description":"Optional extended description in CommonMark or HTML.","responses":{"200":{"description":"A JSON array of user names","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}}}}}}`;
const simpleOpenApiYaml = `openapi: 3.0.0
info:
title: Sample API
paths:
/users:
get:
summary: Returns a list of users.
description: Optional extended description in CommonMark or HTML.
responses:
"200": # status code
description: A JSON array of user names
content:
application/json:
schema:
type: array
items:
type: string`;
describe('SampleLogsInput', () => {
let result: RenderResult;
let input: HTMLElement;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<ApiDefinitionInput
integrationSettings={undefined}
showValidation={false}
isGenerating={false}
onModifySpecFile={() => {}}
/>,
{ wrapper }
);
input = result.getByTestId('apiDefinitionFilePicker');
});
describe('when uploading a json file', () => {
const type = 'application/json';
describe('when the file is a valid json spec file', () => {
beforeEach(async () => {
await changeFile(input, new File([`${simpleOpenApiJson}`], 'test.json', { type }));
});
it('should set the integrationSetting correctly', () => {
expect(mockActions.setIntegrationSettings).toBeCalledWith({
apiSpec: expect.any(Oas),
apiSpecFileName: 'test.json',
});
});
});
describe('when the file is invalid', () => {
const otherJson = `{"events":[\n{"message":"test message 1"},\n{"message":"test message 2"}\n]}`;
beforeEach(async () => {
await changeFile(input, new File([otherJson], 'test.json', { type }));
});
it('should render invalid inputs', () => {
expect(input).toHaveAttribute('aria-invalid');
});
it('should set the integrationSetting correctly', () => {
expect(mockActions.setIntegrationSettings).toBeCalledWith({
apiSpec: undefined,
apiSpecFileName: undefined,
});
});
});
describe('when uploading a yaml spec file', () => {
describe('when the file is a valid yaml spec file', () => {
beforeEach(async () => {
await changeFile(
input,
new File([`${simpleOpenApiYaml}`], 'test.yaml', { type: 'text/yaml' })
);
});
it('should set the integrationSetting correctly', () => {
expect(mockActions.setIntegrationSettings).toBeCalledWith({
apiSpec: expect.any(Oas),
apiSpecFileName: 'test.yaml',
});
});
});
describe('when the file is invalid', () => {
const otherYaml = `foo: 1
bar: 2`;
beforeEach(async () => {
await changeFile(input, new File([otherYaml], 'test.json', { type }));
});
it('should render invalid inputs', () => {
expect(input).toHaveAttribute('aria-invalid');
});
it('should set the integrationSetting correctly', () => {
expect(mockActions.setIntegrationSettings).toBeCalledWith({
apiSpec: undefined,
apiSpecFileName: undefined,
});
});
});
});
describe('when the file is too large', () => {
let jsonParseSpy: jest.SpyInstance;
beforeEach(async () => {
// Simulate large content that would cause a RangeError
jsonParseSpy = jest.spyOn(JSON, 'parse').mockImplementation(() => {
throw new RangeError();
});
await changeFile(input, new File(['...'], 'test.json', { type: 'text/plain' }));
});
afterAll(() => {
// Restore the original implementation after all tests
jsonParseSpy.mockRestore();
});
it('should display invalid', () => {
expect(input).toHaveAttribute('aria-invalid');
});
});
describe('when the file is neither a valid json nor yaml', () => {
const plainTextFile = 'test message 1\ntest message 2';
beforeEach(async () => {
await changeFile(input, new File([plainTextFile], 'test.txt', { type: 'text/plain' }));
});
it('should render invalid inputs', () => {
expect(input).toHaveAttribute('aria-invalid');
});
it('should set the integrationSetting correctly', () => {
expect(mockActions.setIntegrationSettings).toBeCalledWith({
apiSpec: undefined,
apiSpecFileName: undefined,
});
});
});
describe('when the file reader fails', () => {
let myFileReader: FileReader;
let fileReaderSpy: jest.SpyInstance;
beforeEach(async () => {
myFileReader = new FileReader();
fileReaderSpy = jest.spyOn(global, 'FileReader').mockImplementation(() => myFileReader);
jest.spyOn(myFileReader, 'readAsText').mockImplementation(() => {
const errorEvent = new ProgressEvent('error');
myFileReader.dispatchEvent(errorEvent);
});
const file = new File([`...`], 'test.json', { type: 'application/json' });
act(() => {
fireEvent.change(input, { target: { files: [file] } });
});
});
afterEach(() => {
fileReaderSpy.mockRestore();
});
it('should set input invalid', () => {
expect(input).toHaveAttribute('aria-invalid');
});
});
});
});

View file

@ -177,6 +177,7 @@ export const ApiDefinitionInput = React.memo<ApiDefinitionInputProps>(
onChange={onChangeApiDefinition}
display="large"
aria-label="Upload API definition file"
data-loading={isParsing || isGenerating}
isLoading={isParsing || isGenerating}
isInvalid={apiFileError != null || (showValidation && uploadedFile === undefined)}
data-test-subj="apiDefinitionFilePicker"

View file

@ -0,0 +1,127 @@
/*
* 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 React from 'react';
import { act, fireEvent, render, waitFor, type RenderResult } from '@testing-library/react';
import { TestProvider } from '../../../../../../../mocks/test_provider';
import { ActionsProvider } from '../../../../state';
import { mockActions, mockState } from '../../../../mocks/state';
import { UploadSpecStep } from './upload_spec_step';
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
</TestProvider>
);
describe('UploadSpecStep', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when open with initial state', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<UploadSpecStep
integrationSettings={undefined}
connector={mockState.connector}
isFlyoutGenerating={false}
showValidation={false}
onShowValidation={() => {}}
onUpdateValidation={() => {}}
onUpdateNeedsGeneration={() => {}}
onAnalyzeApiGenerationComplete={() => {}}
/>,
{ wrapper }
);
});
it('should render upload spec step', () => {
expect(result.queryByTestId('dataStreamTitleInput')).toBeInTheDocument();
expect(result.queryByTestId('apiDefinitionFilePicker')).toBeInTheDocument();
});
it('analyze button enabled', () => {
expect(result.queryByTestId('analyzeApiButton')).toBeEnabled();
});
});
describe('when opened and validation enabled (clicking submit before filling out the fields)', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<UploadSpecStep
integrationSettings={undefined}
connector={mockState.connector}
isFlyoutGenerating={false}
showValidation={true}
onShowValidation={() => {}}
onUpdateValidation={() => {}}
onUpdateNeedsGeneration={() => {}}
onAnalyzeApiGenerationComplete={() => {}}
/>,
{ wrapper }
);
});
it('analyze button disabled', () => {
expect(result.queryByTestId('analyzeApiButton')).toBeDisabled();
});
describe('fills in fields', () => {
beforeEach(async () => {
await act(async () => {
fireEvent.change(result.getByTestId('dataStreamTitleInput'), {
target: { value: 'testDataStreamTitle' },
});
const filepicker = result.getByTestId('apiDefinitionFilePicker');
fireEvent.change(filepicker, {
target: { files: [new File(['...'], 'test.json', { type: 'application/json' })] },
});
await waitFor(() => expect(filepicker).toHaveAttribute('data-loading', 'true'));
await waitFor(() => expect(filepicker).toHaveAttribute('data-loading', 'false'));
});
});
it('analyze button re-enabled', () => {
expect(result.queryByTestId('analyzeApiButton')).toBeEnabled();
});
});
});
describe('analyzing in progress', () => {
let result: RenderResult;
beforeEach(() => {
jest.clearAllMocks();
result = render(
<UploadSpecStep
integrationSettings={undefined}
connector={mockState.connector}
isFlyoutGenerating={true}
showValidation={false}
onShowValidation={() => {}}
onUpdateValidation={() => {}}
onUpdateNeedsGeneration={() => {}}
onAnalyzeApiGenerationComplete={() => {}}
/>,
{ wrapper }
);
});
it('form is disabled', () => {
expect(result.queryByTestId('dataStreamTitleInput')).toBeDisabled();
});
it('analyze button disabled; cancel button appears and is enabled', () => {
expect(result.queryByTestId('analyzeApiButton')).toBeDisabled();
expect(result.queryByTestId('cancelAnalyzeApiButton')).toBeVisible();
});
});
});

View file

@ -11,7 +11,6 @@ import { TestProvider } from '../../../../mocks/test_provider';
import { Footer } from './footer';
import { ActionsProvider } from '../state';
import { mockActions } from '../mocks/state';
import { ExperimentalFeaturesService } from '../../../../services';
const mockNavigate = jest.fn();
jest.mock('../../../../common/hooks/use_navigate', () => ({
@ -19,9 +18,6 @@ jest.mock('../../../../common/hooks/use_navigate', () => ({
useNavigate: () => mockNavigate,
}));
jest.mock('../../../../services');
const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService);
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<TestProvider>
<ActionsProvider value={mockActions}>{children}</ActionsProvider>
@ -31,10 +27,6 @@ const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
describe('Footer', () => {
beforeEach(() => {
jest.clearAllMocks();
mockedExperimentalFeaturesService.get.mockReturnValue({
generateCel: false,
} as never);
});
describe('when rendered for the most common case', () => {

View file

@ -0,0 +1,33 @@
/*
* 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 { postAnalyzeApi } from '../../../../common/lib/api/analyze_api';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { User } from '../../../../common/lib/authentication/types';
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
describe('Run analyze api', () => {
it('should get 404 when trying to run analyze_api with basic license', async () => {
return await postAnalyzeApi({
supertest,
req: {
dataStreamTitle: 'some data stream',
connectorId: 'bedrock-connector',
pathOptions: {
'/path1': 'Returns the data from path1',
'/path2': 'Returns the data from path2',
},
},
auth: {
user: { username: 'elastic', password: 'elastic' } as User,
},
});
});
});
};

View file

@ -0,0 +1,38 @@
/*
* 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 { postCelInput } from '../../../../common/lib/api/cel';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { User } from '../../../../common/lib/authentication/types';
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
describe('Run cel', () => {
it('should get 404 when trying to run cel with basic license', async () => {
return await postCelInput({
supertest,
req: {
dataStreamTitle: 'some data stream',
connectorId: 'bedrock-connector',
celDetails: {
path: '/path1',
auth: 'basic',
openApiDetails: {
operation: '{ operationJson}',
schemas: '{schemasJson}',
auth: '{authJson}',
},
},
},
auth: {
user: { username: 'elastic', password: 'elastic' } as User,
},
});
});
});
};

View file

@ -0,0 +1,41 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { postAnalyzeApi } from '../../../common/lib/api/analyze_api';
import { User } from '../../../common/lib/authentication/types';
import { BadRequestError } from '../../../common/lib/error/error';
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const supertest = getService('supertest');
describe('Run analyze api', () => {
it('should get 400 when trying to run analyze api without connector action', async () => {
const response = await postAnalyzeApi({
supertest,
req: {
dataStreamTitle: 'some data stream',
connectorId: 'preconfigured-dummy',
pathOptions: {
'/path1': 'Returns the data from path1',
'/path2': 'Returns the data from path2',
},
},
expectedHttpCode: 400,
auth: {
user: { username: 'elastic', password: 'elastic' } as User,
},
});
if (response instanceof BadRequestError) {
expect(response.message).to.be('Saved object [action/preconfigured-dummy] not found');
} else {
expect().fail('Expected BadRequestError');
}
});
});
}

View file

@ -0,0 +1,46 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { postCelInput } from '../../../common/lib/api/cel';
import { User } from '../../../common/lib/authentication/types';
import { BadRequestError } from '../../../common/lib/error/error';
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const supertest = getService('supertest');
describe('Run cel', () => {
it('should get 400 when trying to run cel without connector action', async () => {
const response = await postCelInput({
supertest,
req: {
dataStreamTitle: 'some data stream',
connectorId: 'preconfigured-dummy',
celDetails: {
path: '/path1',
auth: 'basic',
openApiDetails: {
operation: '{ operationJson}',
schemas: '{schemasJson}',
auth: '{authJson}',
},
},
},
expectedHttpCode: 400,
auth: {
user: { username: 'elastic', password: 'elastic' } as User,
},
});
if (response instanceof BadRequestError) {
expect(response.message).to.be('Saved object [action/preconfigured-dummy] not found');
} else {
expect().fail('Expected BadRequestError');
}
});
});
}

View file

@ -0,0 +1,36 @@
/*
* 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 SuperTest from 'supertest';
import {
AnalyzeApiRequestBody,
ANALYZE_API_PATH,
AnalyzeApiResponse,
} from '@kbn/automatic-import-plugin/common';
import { superUser } from '../authentication/users';
import { User } from '../authentication/types';
export const postAnalyzeApi = async ({
supertest,
req,
expectedHttpCode = 404,
auth = { user: superUser },
}: {
supertest: SuperTest.Agent;
req: AnalyzeApiRequestBody;
expectedHttpCode?: number;
auth: { user: User };
}): Promise<AnalyzeApiResponse> => {
const { body: response } = await supertest
.post(`${ANALYZE_API_PATH}`)
.send(req)
.set('kbn-xsrf', 'abc')
.set('elastic-api-version', '1')
.auth(auth.user.username, auth.user.password)
.expect(expectedHttpCode);
return response;
};

View file

@ -0,0 +1,41 @@
/*
* 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 SuperTest from 'supertest';
import {
CelInputRequestBody,
CEL_INPUT_GRAPH_PATH,
CelInputResponse,
} from '@kbn/automatic-import-plugin/common';
import { superUser } from '../authentication/users';
import { User } from '../authentication/types';
import { BadRequestError } from '../error/error';
export const postCelInput = async ({
supertest,
req,
expectedHttpCode = 404,
auth = { user: superUser },
}: {
supertest: SuperTest.Agent;
req: CelInputRequestBody;
expectedHttpCode?: number;
auth: { user: User };
}): Promise<CelInputResponse | BadRequestError> => {
const { body: response } = await supertest
.post(`${CEL_INPUT_GRAPH_PATH}`)
.send(req)
.set('kbn-xsrf', 'abc')
.set('elastic-api-version', '1')
.auth(auth.user.username, auth.user.password)
.expect(expectedHttpCode);
if (response.statusCode === 400) {
return new BadRequestError(response.message);
}
return response;
};