Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solutions][Cases - Timeline] Fix bug when adding a timeline to a case #76967

Merged
merged 11 commits into from
Sep 15, 2020
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens

import { goToCaseDetails, goToCreateNewCase } from '../tasks/all_cases';
import { openCaseTimeline } from '../tasks/case_details';
import { backToCases, createNewCase } from '../tasks/create_new_case';
import { backToCases, createNewCaseWithTimeline } from '../tasks/create_new_case';
import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';

@@ -58,7 +58,7 @@ describe('Cases', () => {
it('Creates a new case with timeline and opens the timeline', () => {
loginAndWaitForPageWithoutDateRange(CASES_URL);
goToCreateNewCase();
createNewCase(case1);
createNewCaseWithTimeline(case1);
backToCases();

cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta');
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { loginAndWaitForTimeline } from '../tasks/login';
import {
attachTimelineToNewCase,
attachTimelineToExistingCase,
addNewCase,
selectCase,
} from '../tasks/timeline';
import { DESCRIPTION_INPUT } from '../screens/create_new_case';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { caseTimeline, TIMELINE_CASE_ID } from '../objects/case';

describe('attach timeline to case', () => {
beforeEach(() => {
loginAndWaitForTimeline(caseTimeline.id);
});
context('without cases created', () => {
before(() => {
esArchiverLoad('timeline');
});

after(() => {
esArchiverUnload('timeline');
});

it('attach timeline to a new case', () => {
attachTimelineToNewCase();

cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
'have.text',
`[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
);
});
});

it('attach timeline to an existing case with no case', () => {
attachTimelineToExistingCase();
addNewCase();

cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
'have.text',
`[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
);
});
});
});

context('with cases created', () => {
before(() => {
esArchiverLoad('case_and_timeline');
});

after(() => {
esArchiverUnload('case_and_timeline');
});

it('attach timeline to an existing case', () => {
attachTimelineToExistingCase();
selectCase(TIMELINE_CASE_ID);

cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
'have.text',
`[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
);
});
});
});
});
7 changes: 5 additions & 2 deletions x-pack/plugins/security_solution/cypress/objects/case.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Timeline } from './timeline';
import { Timeline, TimelineWithId } from './timeline';

export interface TestCase {
name: string;
@@ -21,10 +21,11 @@ export interface Connector {
password: string;
}

const caseTimeline: Timeline = {
export const caseTimeline: TimelineWithId = {
title: 'SIEM test',
description: 'description',
query: 'host.name:*',
id: '0162c130-78be-11ea-9718-118a926974a4',
};

export const case1: TestCase = {
@@ -41,3 +42,5 @@ export const serviceNowConnector: Connector = {
username: 'Username Name',
password: 'password',
};

export const TIMELINE_CASE_ID = '68248e00-f689-11ea-9ab2-59238b522856';
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/cypress/objects/timeline.ts
Original file line number Diff line number Diff line change
@@ -9,3 +9,7 @@ export interface Timeline {
description: string;
query: string;
}

export interface TimelineWithId extends Timeline {
id: string;
}
6 changes: 6 additions & 0 deletions x-pack/plugins/security_solution/cypress/screens/all_cases.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/

export const ALL_CASES_CASE = (id: string) => {
return `[data-test-subj="cases-table-row-${id}"]`;
};

export const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]';

export const ALL_CASES_CLOSED_CASES_COUNT = '[data-test-subj="closed-case-count"]';
@@ -14,6 +18,8 @@ export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-comm

export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn"]';

export const ALL_CASES_CREATE_NEW_CASE_TABLE_BTN = '[data-test-subj="cases-table-add-case"]';

export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]';

export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]';
Original file line number Diff line number Diff line change
@@ -6,8 +6,7 @@

export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]';

export const DESCRIPTION_INPUT =
'[data-test-subj="caseDescription"] [data-test-subj="textAreaInput"]';
export const DESCRIPTION_INPUT = '[data-test-subj="textAreaInput"]';

export const INSERT_TIMELINE_BTN = '[data-test-subj="insert-timeline-button"]';

11 changes: 11 additions & 0 deletions x-pack/plugins/security_solution/cypress/screens/timeline.ts
Original file line number Diff line number Diff line change
@@ -4,8 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/

export const ATTACH_TIMELINE_TO_NEW_CASE_ICON = '[data-test-subj="attach-timeline-case"]';

export const ATTACH_TIMELINE_TO_EXISTING_CASE_ICON =
'[data-test-subj="attach-timeline-existing-case"]';

export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]';

export const CASE = (id: string) => {
return `[data-test-subj="cases-table-row-${id}"]`;
};

export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]';

export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]';
@@ -25,6 +34,8 @@ export const ID_FIELD = '[data-test-subj="timeline"] [data-test-subj="field-name

export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]';

export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]';

export const PIN_EVENT = '[data-test-subj="pin"]';

export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]';
12 changes: 12 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,18 @@ export const createNewCase = (newCase: TestCase) => {
});
cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true });

cy.get(SUBMIT_BTN).click({ force: true });
cy.get(LOADING_SPINNER).should('exist');
cy.get(LOADING_SPINNER).should('not.exist');
};

export const createNewCaseWithTimeline = (newCase: TestCase) => {
cy.get(TITLE_INPUT).type(newCase.name, { force: true });
newCase.tags.forEach((tag) => {
cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true });
});
cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true });

cy.get(INSERT_TIMELINE_BTN).click({ force: true });
cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`);
cy.get(TIMELINE).should('be.visible');
9 changes: 9 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/login.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
*/

import * as yaml from 'js-yaml';
import { TIMELINE_FLYOUT_BODY } from '../screens/timeline';

/**
* Credentials in the `kibana.dev.yml` config file will be used to authenticate
@@ -143,3 +144,11 @@ export const loginAndWaitForPageWithoutDateRange = (url: string) => {
cy.visit(url);
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
};

export const loginAndWaitForTimeline = (timelineId: string) => {
login();
cy.viewport('macbook-15');
cy.visit(`/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`);
cy.get('[data-test-subj="headerGlobalNav"]');
cy.get(TIMELINE_FLYOUT_BODY).should('be.visible');
};
28 changes: 28 additions & 0 deletions x-pack/plugins/security_solution/cypress/tasks/timeline.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases';
import {
BULK_ACTIONS,
CLOSE_TIMELINE_BTN,
@@ -28,6 +29,10 @@ import {
TOGGLE_TIMELINE_EXPAND_EVENT,
REMOVE_COLUMN,
RESET_FIELDS,
ATTACH_TIMELINE_TO_NEW_CASE_ICON,
OPEN_TIMELINE_ICON,
ATTACH_TIMELINE_TO_EXISTING_CASE_ICON,
CASE,
} from '../screens/timeline';

import { drag, drop } from '../tasks/common';
@@ -44,6 +49,20 @@ export const addNameToTimeline = (name: string) => {
cy.get(TIMELINE_TITLE).should('have.attr', 'value', name);
};

export const addNewCase = () => {
cy.get(ALL_CASES_CREATE_NEW_CASE_TABLE_BTN).click();
};

export const attachTimelineToNewCase = () => {
cy.get(TIMELINE_SETTINGS_ICON).click({ force: true });
cy.get(ATTACH_TIMELINE_TO_NEW_CASE_ICON).click({ force: true });
};

export const attachTimelineToExistingCase = () => {
cy.get(TIMELINE_SETTINGS_ICON).click({ force: true });
cy.get(ATTACH_TIMELINE_TO_EXISTING_CASE_ICON).click({ force: true });
};

export const checkIdToggleField = () => {
cy.get(ID_HEADER_FIELD).should('not.exist');

@@ -85,6 +104,11 @@ export const openTimelineInspectButton = () => {
cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true });
};

export const openTimelineFromSettings = () => {
cy.get(TIMELINE_SETTINGS_ICON).click({ force: true });
cy.get(OPEN_TIMELINE_ICON).click({ force: true });
};

export const openTimelineSettings = () => {
cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true });
};
@@ -132,6 +156,10 @@ export const resetFields = () => {
cy.get(RESET_FIELDS).click({ force: true });
};

export const selectCase = (caseId: string) => {
cy.get(CASE(caseId)).click();
};

export const waitForTimelinesPanelToBeLoaded = () => {
cy.get(TIMELINES_TABLE).should('exist');
};
Original file line number Diff line number Diff line change
@@ -4,9 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form';
import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data';

jest.mock(
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
);
jest.mock(
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'
);

export const mockFormHook = {
isSubmitted: false,
isSubmitting: false,
@@ -41,3 +47,4 @@ export const getFormMock = (sampleData: any) => ({
});

export const useFormMock = useForm as jest.Mock;
export const useFormDataMock = useFormData as jest.Mock;
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
import { usePostComment } from '../../containers/use_post_comment';
import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form';
import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data';

// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
import { wait as waitFor } from '@testing-library/react';
@@ -23,10 +24,15 @@ jest.mock(
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
);

jest.mock(
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'
);

jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline');
jest.mock('../../containers/use_post_comment');

export const useFormMock = useForm as jest.Mock;
const useFormMock = useForm as jest.Mock;
const useFormDataMock = useFormData as jest.Mock;

const useInsertTimelineMock = useInsertTimeline as jest.Mock;
const usePostCommentMock = usePostComment as jest.Mock;
@@ -73,6 +79,7 @@ describe('AddComment ', () => {
useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
usePostCommentMock.mockImplementation(() => defaultPostCommment);
useFormMock.mockImplementation(() => ({ form: formHookMock }));
useFormDataMock.mockImplementation(() => [{ comment: sampleData.comment }]);
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
});

Loading