[Enterprise Search] MongoDB Connector cypress test. (#155886)

## Summary

Add automated tests via Cypress for MongoDB and Web Crawler happy paths.

Tests are quite basic and covers only adding simple one time sync paths.
Fill in the `cypress.env.json` with the credentials before running.
Mongo specs will run only against a local environment (via `./cypress.sh
dev`) as connector has to be running locally as well.


### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Efe Gürkan YALAMAN 2023-04-28 11:21:45 +02:00 committed by GitHub
parent 8c87a3f751
commit a4953edcad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 299 additions and 12 deletions

View file

@ -0,0 +1,12 @@
{
"mongo_test": {
"host": "",
"username": "",
"password": "",
"database": "",
"collection": ""
},
"crawler_test": {
"domain": ""
}
}

View file

@ -7,9 +7,55 @@
import { login } from '../../tasks/login';
import { NEW_INDEX_CARD, CRAWLER_INDEX, INDEX_OVERVIEW, ROUTES, getIndexRoute } from './selectors';
describe('Enterprise Search Crawler', () => {
it('test', () => {
const envConfig = Cypress.env('crawler_test');
const indexName = 'cypress-crawler-' + Math.random();
login();
cy.visit('/app/enterprise_search/content/search_indices/new_index');
cy.visit(ROUTES.NEW_INDEX);
// Crawler selected by default
cy.getBySel(NEW_INDEX_CARD.SELECT_CRAWLER).click();
// we are in correct route
cy.url().should('contain', ROUTES.NEW_INDEX);
cy.getBySel(CRAWLER_INDEX.CREATE_BUTTON).should('be.disabled');
// type new name
cy.getBySel(CRAWLER_INDEX.INDEX_NAME_INPUT).type(indexName);
// create index
cy.getBySel(CRAWLER_INDEX.CREATE_BUTTON).click();
// make sure we are in new index
cy.url().should('contain', getIndexRoute(indexName) + 'domain_management');
cy.getBySel(CRAWLER_INDEX.DOMAIN_MANAGEMENT.DOMAIN_INPUT).type(envConfig.domain);
cy.getBySel(CRAWLER_INDEX.DOMAIN_MANAGEMENT.DOMAIN_BUTTON).click();
cy.getBySel(CRAWLER_INDEX.DOMAIN_MANAGEMENT.SUBMIT_BUTTON).should('be.enabled');
cy.getBySel(CRAWLER_INDEX.DOMAIN_MANAGEMENT.SUBMIT_BUTTON).click();
cy.getBySel(CRAWLER_INDEX.CRAWL_DROPDOWN).should('be.enabled');
cy.getBySel(CRAWLER_INDEX.CRAWL_DROPDOWN).click();
cy.getBySel(CRAWLER_INDEX.CRAWL_ALL_DOMAINS).click();
// go to overview tab
cy.getBySel(INDEX_OVERVIEW.TABS.OVERVIEW).click();
// Page header has index name
cy.get('main header h1').should('contain.text', indexName);
// check Ingestion Type stat is Crawler
cy.getBySel(INDEX_OVERVIEW.STATS.INGESTION_TYPE).should('contain.text', 'Crawler');
cy.getBySel(INDEX_OVERVIEW.STATS.DOCUMENT_COUNT).should((el) => {
const text = el.text();
const count = parseInt(text.match(/[0-9]+/g), 10);
expect(count).to.gt(0);
});
});
});

View file

@ -0,0 +1,95 @@
/*
* 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 { login } from '../../../tasks/login';
import {
CONNECTOR_INDEX,
getIndexRoute,
INDEX_OVERVIEW,
NEW_INDEX_CARD,
NEW_CONNECTOR_PAGE,
ROUTES,
SELECT_CONNECTOR,
SEARCH_INDICES,
} from '../selectors';
describe('Enterprise Search MongoDB connector', () => {
it('succesfully syncs documents with single sync', () => {
// Get configuration information from cypress.env.json
const mongoConfig = Cypress.env('mongo_test');
const indexName = 'cypress-mongodb-' + Math.random();
const baseUrl = Cypress.config().baseUrl;
login();
cy.visit(ROUTES.SEARCH_INDICES_OVERVIEW);
cy.getBySel(SEARCH_INDICES.CREATE_INDEX_BUTTON).click();
cy.url().should('eq', baseUrl + ROUTES.NEW_INDEX);
// select connector
cy.getBySel(NEW_INDEX_CARD.SELECT_CONNECTOR).click();
// we are in correct route
cy.url().should('contain', ROUTES.SELECT_CONNECTOR);
// Select MongoDB from the list
cy.get('#checkableCard-mongodb').should('not.be.selected');
cy.get('#checkableCard-mongodb-details')
.find('a')
.invoke('attr', 'href')
.should('include', 'connectors-mongodb.html');
cy.get('#checkableCard-mongodb').click();
cy.getBySel(SELECT_CONNECTOR.SELECT_AND_CONFIGURE_BUTTON).click();
// Connector URL, mongo selected
cy.url().should('contain', 'service_type=mongodb');
cy.getBySel(NEW_CONNECTOR_PAGE.INDEX_NAME_INPUT).type(indexName);
// create index
cy.getBySel(NEW_CONNECTOR_PAGE.CREATE_BUTTON).click();
// make sure we are in new index route
cy.url().should('contain', getIndexRoute(indexName) + 'configuration');
// Fill in connector configuration
cy.getBySel(CONNECTOR_INDEX.getConfigurationRow('host')).type(mongoConfig.host);
cy.getBySel(CONNECTOR_INDEX.getConfigurationRow('user')).type(mongoConfig.username);
cy.getBySel(CONNECTOR_INDEX.getConfigurationRow('password')).type(mongoConfig.password);
cy.getBySel(CONNECTOR_INDEX.getConfigurationRow('database')).type(mongoConfig.database);
cy.getBySel(CONNECTOR_INDEX.getConfigurationRow('collection')).type(mongoConfig.collection);
cy.getBySel(CONNECTOR_INDEX.SAVE_CONFIG).click();
// Wait until configuration is saved
cy.getBySel(CONNECTOR_INDEX.EDIT_CONFIG);
cy.getBySel(CONNECTOR_INDEX.SET_SCHEDULE_BUTTON).click();
// Scheduling Tab opened
cy.url().should('contain', getIndexRoute(indexName) + 'scheduling');
// Start one time sync
cy.getBySel(CONNECTOR_INDEX.HEADER_SYNC_MENU).click();
cy.getBySel(CONNECTOR_INDEX.HEADER_SYNC_MENU_START).click();
// go to overview tab
cy.getBySel(INDEX_OVERVIEW.TABS.OVERVIEW).click();
cy.getBySel(INDEX_OVERVIEW.STATS.INGESTION_TYPE).should('contain.text', 'Connector');
cy.getBySel(INDEX_OVERVIEW.STATS.CONNECTOR_TYPE).should('contain.text', 'MongoDB');
cy.getBySel(INDEX_OVERVIEW.STATS.INGESTION_STATUS).should('contain.text', 'Configured');
cy.getBySel(INDEX_OVERVIEW.STATS.INGESTION_STATUS).should('contain.text', 'Connected');
// Wait until document count > 0
cy.getBySel(INDEX_OVERVIEW.STATS.DOCUMENT_COUNT).should((el) => {
const text = el.text();
const count = parseInt(text.match(/[0-9]+/g), 10);
expect(count).to.gt(0);
});
});
});

View file

@ -0,0 +1,70 @@
/*
* 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.
*/
export const ROUTES = {
CRAWLER_INDEX: '/app/enterprise_search/content/search_indices/new_index/crawler',
NEW_INDEX: '/app/enterprise_search/content/search_indices/new_index',
SEARCH_INDICES_OVERVIEW: '/app/enterprise_search/content/search_indices/',
SELECT_CONNECTOR: '/app/enterprise_search/content/search_indices/new_index/select_connector',
};
export const SEARCH_INDICES = {
CREATE_INDEX_BUTTON: 'entSearchContent-searchIndices-createButton',
};
export const SELECT_CONNECTOR = {
SELECT_AND_CONFIGURE_BUTTON: 'entSearchContent-connector-selectConnector-selectAndConfigure',
};
export const NEW_CONNECTOR_PAGE = {
CREATE_BUTTON: 'entSearchContent-connector-newIndex-createIndex',
INDEX_NAME_INPUT: 'entSearchContent-connector-newIndex-editName',
};
export const CONNECTOR_INDEX = {
EDIT_CONFIG: 'entSearchContent-connector-configuration-editConfiguration',
HEADER_SYNC_MENU: 'entSearchContent-connector-header-sync-menu',
HEADER_SYNC_MENU_START: 'entSearchContent-connector-header-sync-startSync',
SAVE_CONFIG: 'entSearchContent-connector-configuration-saveConfiguration',
SET_SCHEDULE_BUTTON: 'entSearchContent-connector-configuration-setScheduleAndSync',
getConfigurationRow: (rowkey: string) =>
`entSearchContent-connector-configuration-formrow-${rowkey}`,
};
export const NEW_INDEX_CARD = {
SELECT_CONNECTOR: 'entSearchContent-newIndexCard-button-connector',
SELECT_CRAWLER: 'entSearchContent-newIndexCard-button-crawler',
};
export const CRAWLER_INDEX = {
CRAWL_ALL_DOMAINS: 'entSearchContent-crawler-startCrawlMenu-crawlAllDomains',
CRAWL_DROPDOWN: 'entSearchContent-crawler-startCrawlMenu-menuButton',
CREATE_BUTTON: 'entSearchContent-crawler-newIndex-createIndex',
DOMAIN_MANAGEMENT: {
DOMAIN_BUTTON: 'entSearchContent-crawler-addDomainForm-validate-button',
DOMAIN_INPUT: 'entSearchContent-crawler-addDomainForm-validate-input',
SUBMIT_BUTTON: 'entSearchContent-crawler-addDomain-submitButton',
},
INDEX_NAME_INPUT: 'entSearchContent-crawler-newIndex-editName',
};
export const INDEX_OVERVIEW = {
STATS: {
CONNECTOR_TYPE: 'entSearchContent-indexOverview-totalStats-connectorType',
DOCUMENT_COUNT: 'entSearchContent-indexOverview-totalStats-documentCount',
INGESTION_STATUS: 'entSearchContent-indexOverview-connectorStats-ingestionStatus',
INGESTION_TYPE: 'entSearchContent-indexOverview-totalStats-ingestionType',
},
TABS: {
CRAWLER_SCHEDULER: 'entSearchContent-index-crawler-scheduler-tab',
OVERVIEW: 'entSearchContent-index-overview-tab',
},
};
export const getIndexRoute = (indexName: string) => {
return `/app/enterprise_search/content/search_indices/search-${indexName}/`;
};

View file

@ -29,9 +29,10 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
findBySel(value: string, ...args: any[]): Chainable<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getBySel(value: string, ...args: any[]): Chainable<any>;
getKibanaVersion(): Chainable<string>;
}
}
}
@ -41,7 +42,13 @@ function getBySel(selector: string, ...args: any[]) {
return cy.get(`[data-test-subj="${selector}"]`, ...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function findBySel(selector: string, ...args: any[]) {
return cy.find(`[data-test-subj="${selector}"]`, ...args);
}
Cypress.Commands.add('getBySel', getBySel);
Cypress.Commands.add('findBySel', findBySel);
Cypress.on('uncaught:exception', () => {
return false;

View file

@ -97,6 +97,7 @@ export const NewIndexCard: React.FC<NewIndexCardProps> = ({ onSelect, isSelected
return (
<EuiCard
data-test-subj="entSearch-content-newIndexCard-cardBody"
hasBorder
icon={<EuiIcon type={icon} size="xxl" />}
title={title}
@ -110,6 +111,7 @@ export const NewIndexCard: React.FC<NewIndexCardProps> = ({ onSelect, isSelected
</>
)}
<EuiButton
data-test-subj={`entSearchContent-newIndexCard-button-${type}`}
fullWidth
onClick={onSelect}
color={isSelected ? 'success' : 'primary'}

View file

@ -136,6 +136,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({
fullWidth
>
<EuiFieldText
data-test-subj={`entSearchContent-${type}-newIndex-editName`}
data-telemetry-id={`entSearchContent-${type}-newIndex-editName`}
placeholder={i18n.translate(
'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.nameInputPlaceholder',
@ -253,6 +254,7 @@ export const NewSearchIndexTemplate: React.FC<Props> = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj={`entSearchContent-${type}-newIndex-createIndex`}
data-telemetry-id={`entSearchContent-${type}-newIndex-createIndex`}
fill
isDisabled={!rawName || buttonLoading || formInvalid || disabled}

View file

@ -117,6 +117,7 @@ export const SelectConnector: React.FC = () => {
<EuiFlexItem grow={false}>
<span>
<EuiButton
data-test-subj="entSearchContent-connector-selectConnector-selectAndConfigure"
data-telemetry-id="entSearchContent-connector-selectConnector-selectAndConfigure"
disabled={!selectedConnector}
fill

View file

@ -75,7 +75,9 @@ export const SyncsContextMenu: React.FC = () => {
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
)}
<EuiFlexItem>{getSyncButtonText()}</EuiFlexItem>
<EuiFlexItem data-test-subj={`entSearchContent-${ingestionMethod}-header-sync-menu`}>
{getSyncButtonText()}
</EuiFlexItem>
</EuiFlexGroup>
</EuiButton>
}
@ -90,6 +92,7 @@ export const SyncsContextMenu: React.FC = () => {
? []
: [
<EuiContextMenuItem
data-test-subj={`entSearchContent-${ingestionMethod}-header-sync-startSync`}
data-telemetry-id={`entSearchContent-${ingestionMethod}-header-sync-startSync`}
disabled={ingestionStatus === IngestionStatus.INCOMPLETE}
key="Sync"

View file

@ -275,6 +275,7 @@ export const ConnectorConfiguration: React.FC = () => {
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonTo
data-test-subj="entSearchContent-connector-configuration-setScheduleAndSync"
data-telemetry-id="entSearchContent-connector-configuration-setScheduleAndSync"
to={`${generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,

View file

@ -51,6 +51,7 @@ export const ConnectorConfigurationConfig: React.FC = ({ children }) => {
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-editConfiguration"
data-telemetry-id="entSearchContent-connector-overview-configuration-editConfiguration"
onClick={() => setIsEditing(!isEditing)}
>

View file

@ -89,7 +89,12 @@ export const ConnectorConfigurationForm = () => {
<>
{topSpacing}
<EuiPanel color="subdued" borderRadius="none">
<EuiFormRow label={rowLabel} key={key} helpText={helpText}>
<EuiFormRow
label={rowLabel}
key={key}
helpText={helpText}
data-test-subj={`entSearchContent-connector-configuration-formrow-${key}`}
>
<ConnectorConfigurationField configEntry={configEntry} />
</EuiFormRow>
</EuiPanel>
@ -99,7 +104,12 @@ export const ConnectorConfigurationForm = () => {
}
return (
<EuiFormRow label={rowLabel} key={key} helpText={helpText}>
<EuiFormRow
label={rowLabel}
key={key}
helpText={helpText}
data-test-subj={`entSearchContent-connector-configuration-formrow-${key}`}
>
<ConnectorConfigurationField configEntry={configEntry} />
</EuiFormRow>
);
@ -108,6 +118,7 @@ export const ConnectorConfigurationForm = () => {
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="entSearchContent-connector-configuration-saveConfiguration"
data-telemetry-id="entSearchContent-connector-configuration-saveConfiguration"
type="submit"
isLoading={status === Status.LOADING}

View file

@ -48,6 +48,7 @@ export const ConnectorOverviewPanels: React.FC = () => {
<EuiFlexItem grow={1}>
<EuiPanel color="primary" hasShadow={false} paddingSize="l">
<EuiStat
data-test-subj="entSearchContent-indexOverview-totalStats-documentCount"
titleSize="m"
description={i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.totalStats.documentCountCardLabel',
@ -59,7 +60,10 @@ export const ConnectorOverviewPanels: React.FC = () => {
/>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiFlexItem
grow={1}
data-test-subj="entSearchContent-indexOverview-connectorStats-ingestionStatus"
>
{ingestionStatus === IngestionStatus.INCOMPLETE ? (
<EuiLinkTo
to={generateEncodedPath(SEARCH_INDEX_TAB_PATH, {

View file

@ -39,6 +39,7 @@ export const NativeConnectorAdvancedConfiguration: React.FC = () => {
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonTo
data-test-subj="entSearchContent-connector-configuration-setScheduleAndSync"
data-telemetry-id="entSearchContent-connector-configuration-setScheduleAndSync"
to={`${generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,

View file

@ -36,8 +36,9 @@ export const ConnectorTotalStats: React.FC = () => {
return <></>;
}
const stats: EuiStatProps[] = [
const stats: EuiStatProps[] & { 'data-test-subj'?: string } = [
{
'data-test-subj': 'entSearchContent-indexOverview-totalStats-ingestionType',
description: i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.totalStats.ingestionTypeCardLabel',
{
@ -53,6 +54,7 @@ export const ConnectorTotalStats: React.FC = () => {
),
},
{
'data-test-subj': 'entSearchContent-indexOverview-totalStats-connectorType',
description: i18n.translate('xpack.enterpriseSearch.connector.connectorTypePanel.title', {
defaultMessage: 'Connector type',
}),

View file

@ -66,11 +66,17 @@ export const AddDomainForm: React.FC = () => {
value={addDomainFormInputValue}
onChange={(e) => setAddDomainFormInputValue(e.target.value)}
fullWidth
data-test-subj="entSearchContent-crawler-addDomainForm-validate-input"
/>
</EuiFormControlLayout>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton type="submit" fill disabled={addDomainFormInputValue.length === 0}>
<EuiButton
type="submit"
fill
disabled={addDomainFormInputValue.length === 0}
data-test-subj="entSearchContent-crawler-addDomainForm-validate-button"
>
{i18n.translate(
'xpack.enterpriseSearch.crawler.addDomainForm.validateButtonLabel',
{

View file

@ -21,7 +21,13 @@ export const AddDomainFormSubmitButton: React.FC = () => {
const { allowSubmit } = useValues(AddDomainLogic);
return (
<EuiButton fill type="button" disabled={!allowSubmit} onClick={submitNewDomain}>
<EuiButton
fill
type="button"
disabled={!allowSubmit}
onClick={submitNewDomain}
data-test-subj="entSearchContent-crawler-addDomain-submitButton"
>
{i18n.translate('xpack.enterpriseSearch.crawler.addDomainForm.submitButtonLabel', {
defaultMessage: 'Add domain',
})}

View file

@ -29,8 +29,9 @@ export const CrawlerTotalStats: React.FC = () => {
const documentCount = indexData?.count ?? 0;
const hideStats = isLoading || isError;
const stats: EuiStatProps[] = [
const stats: EuiStatProps[] & { 'data-test-subj'?: string } = [
{
'data-test-subj': 'entSearchContent-indexOverview-totalStats-ingestionType',
description: i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.totalStats.ingestionTypeCardLabel',
{
@ -56,6 +57,7 @@ export const CrawlerTotalStats: React.FC = () => {
title: domains.length,
},
{
'data-test-subj': 'entSearchContent-indexOverview-totalStats-documentCount',
description: i18n.translate(
'xpack.enterpriseSearch.content.searchIndex.totalStats.documentCountCardLabel',
{

View file

@ -98,6 +98,7 @@ export const SearchIndex: React.FC = () => {
const ALL_INDICES_TABS: EuiTabbedContentTab[] = [
{
content: <SearchIndexOverview />,
'data-test-subj': 'entSearchContent-index-overview-tab',
id: SearchIndexTabId.OVERVIEW,
name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.overviewTabLabel', {
defaultMessage: 'Overview',
@ -167,6 +168,7 @@ export const SearchIndex: React.FC = () => {
},
{
content: <AutomaticCrawlScheduler />,
'data-test-subj': 'entSearchContent-index-crawler-scheduler-tab',
id: SearchIndexTabId.SCHEDULING,
name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.schedulingTabLabel', {
defaultMessage: 'Scheduling',

View file

@ -115,6 +115,7 @@ export const SyncJobs: React.FC = () => {
<>
<SyncJobFlyout onClose={() => setSyncJobFlyout(undefined)} syncJob={syncJobFlyout} />
<EuiBasicTable
data-test-subj="entSearchContent-index-syncJobs-table"
items={syncJobs}
columns={columns}
hasActions

View file

@ -106,7 +106,12 @@ export const SearchIndices: React.FC = () => {
? []
: [
<EuiLinkTo data-test-subj="create-new-index-button" to={NEW_INDEX_PATH}>
<EuiButton iconType="plusInCircle" color="primary" fill>
<EuiButton
iconType="plusInCircle"
color="primary"
fill
data-test-subj="entSearchContent-searchIndices-createButton"
>
{i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.create.buttonTitle',
{

View file

@ -25,7 +25,13 @@ export const StartCrawlContextMenu: React.FC = () => {
return (
<EuiPopover
button={
<EuiButton iconType="arrowDown" iconSide="right" onClick={togglePopover} fill>
<EuiButton
iconType="arrowDown"
iconSide="right"
onClick={togglePopover}
fill
data-test-subj="entSearchContent-crawler-startCrawlMenu-menuButton"
>
{i18n.translate(
'xpack.enterpriseSearch.crawler.crawlerStatusIndicator.retryCrawlButtonLabel',
{
@ -48,6 +54,7 @@ export const StartCrawlContextMenu: React.FC = () => {
startCrawl();
}}
icon="play"
data-test-subj="entSearchContent-crawler-startCrawlMenu-crawlAllDomains"
>
{i18n.translate(
'xpack.enterpriseSearch.crawler.startCrawlContextMenu.crawlAllDomainsMenuLabel',