Skip to content

Commit

Permalink
[SecuritySolution] Add timeline middleware tests (#178009)
Browse files Browse the repository at this point in the history
## Summary

In the previous work for
#175427, we replaced
redux-observable with plain redux middlewares. The code that was based
on redux-observable wasn't tested, so as part of the refactoring we're
now adding tests to all timeline middlewares in this PR.


### Checklist

- [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
  • Loading branch information
janmonschke authored Mar 6, 2024
1 parent bb33687 commit 8f39000
Show file tree
Hide file tree
Showing 7 changed files with 679 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 { createMockStore } from '../../../common/mock';
import { selectTimelineById } from '../selectors';
import { TimelineId } from '../../../../common/types/timeline';

import {
setChanged,
updateKqlMode,
showTimeline,
applyKqlFilterQuery,
addProvider,
dataProviderEdited,
removeColumn,
removeProvider,
updateColumns,
updateEqlOptions,
updateDataProviderEnabled,
updateDataProviderExcluded,
updateDataProviderType,
updateProviders,
updateRange,
updateSort,
upsertColumn,
updateDataView,
updateTitleAndDescription,
setExcludedRowRendererIds,
setFilters,
setSavedQueryId,
updateSavedSearch,
} from '../actions';
import { timelineChangedTypes } from './timeline_changed';

jest.mock('../actions', () => {
const actual = jest.requireActual('../actions');
return {
...actual,
setChanged: jest.fn().mockImplementation((...args) => actual.setChanged(...args)),
};
});

/**
* This is a copy of the timeline changed types from the actual middleware.
* The purpose of this copy is to enforce changes to the original to fail.
* These changes will need to be applied to the copy to pass the tests.
* That way, we are preventing accidental changes to the original.
*/
const timelineChangedTypesCopy = [
applyKqlFilterQuery.type,
addProvider.type,
dataProviderEdited.type,
removeProvider.type,
setExcludedRowRendererIds.type,
setFilters.type,
setSavedQueryId.type,
updateDataProviderEnabled.type,
updateDataProviderExcluded.type,
updateDataProviderType.type,
updateEqlOptions.type,
updateKqlMode.type,
updateProviders.type,
updateTitleAndDescription.type,

updateDataView.type,
removeColumn.type,
updateColumns.type,
updateSort.type,
updateRange.type,
upsertColumn.type,

updateSavedSearch.type,
];

const setChangedMock = setChanged as unknown as jest.Mock;

describe('Timeline changed middleware', () => {
let store = createMockStore();

beforeEach(() => {
store = createMockStore();
setChangedMock.mockClear();
});

it('should mark a timeline as changed for some actions', () => {
expect(selectTimelineById(store.getState(), TimelineId.test).kqlMode).toEqual('filter');

store.dispatch(updateKqlMode({ id: TimelineId.test, kqlMode: 'search' }));

expect(setChangedMock).toHaveBeenCalledWith({ id: TimelineId.test, changed: true });
expect(selectTimelineById(store.getState(), TimelineId.test).kqlMode).toEqual('search');
});

it('should check that all correct actions are used to check for changes', () => {
timelineChangedTypesCopy.forEach((changedType) => {
expect(timelineChangedTypes.has(changedType)).toBeTruthy();
});
});

it('should not mark a timeline as changed for some actions', () => {
store.dispatch(showTimeline({ id: TimelineId.test, show: true }));
expect(setChangedMock).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
/**
* All action types that will mark a timeline as changed
*/
const timelineChangedTypes = new Set([
export const timelineChangedTypes = new Set([
applyKqlFilterQuery.type,
addProvider.type,
dataProviderEdited.type,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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 { createMockStore, kibanaMock } from '../../../common/mock';
import { selectTimelineById } from '../selectors';
import { persistFavorite } from '../../containers/api';
import { TimelineId } from '../../../../common/types/timeline';
import { refreshTimelines } from './helpers';

import {
startTimelineSaving,
endTimelineSaving,
updateIsFavorite,
showCallOutUnauthorizedMsg,
updateTimeline,
} from '../actions';

jest.mock('../actions', () => {
const actual = jest.requireActual('../actions');
const endTLSaving = jest.fn((...args) => actual.endTimelineSaving(...args));
(endTLSaving as unknown as { match: Function }).match = () => false;
return {
...actual,
showCallOutUnauthorizedMsg: jest
.fn()
.mockImplementation((...args) => actual.showCallOutUnauthorizedMsg(...args)),
startTimelineSaving: jest
.fn()
.mockImplementation((...args) => actual.startTimelineSaving(...args)),
endTimelineSaving: endTLSaving,
};
});
jest.mock('../../containers/api');
jest.mock('./helpers');

const startTimelineSavingMock = startTimelineSaving as unknown as jest.Mock;
const endTimelineSavingMock = endTimelineSaving as unknown as jest.Mock;
const showCallOutUnauthorizedMsgMock = showCallOutUnauthorizedMsg as unknown as jest.Mock;

describe('Timeline favorite middleware', () => {
let store = createMockStore(undefined, undefined, kibanaMock);
const newVersion = 'new_version';
const newSavedObjectId = 'new_so_id';

beforeEach(() => {
store = createMockStore(undefined, undefined, kibanaMock);
jest.clearAllMocks();
});

it('should persist a timeline favorite when a favorite action is dispatched', async () => {
(persistFavorite as jest.Mock).mockResolvedValue({
data: {
persistFavorite: {
code: 200,
favorite: [{}],
savedObjectId: newSavedObjectId,
version: newVersion,
},
},
});
expect(selectTimelineById(store.getState(), TimelineId.test).isFavorite).toEqual(false);
await store.dispatch(updateIsFavorite({ id: TimelineId.test, isFavorite: true }));

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(refreshTimelines as unknown as jest.Mock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(selectTimelineById(store.getState(), TimelineId.test)).toEqual(
expect.objectContaining({
isFavorite: true,
savedObjectId: newSavedObjectId,
version: newVersion,
})
);
});

it('should persist a timeline un-favorite when a favorite action is dispatched for a favorited timeline', async () => {
store.dispatch(
updateTimeline({
id: TimelineId.test,
timeline: {
...selectTimelineById(store.getState(), TimelineId.test),
isFavorite: true,
},
})
);
(persistFavorite as jest.Mock).mockResolvedValue({
data: {
persistFavorite: {
code: 200,
favorite: [],
savedObjectId: newSavedObjectId,
version: newVersion,
},
},
});
expect(selectTimelineById(store.getState(), TimelineId.test).isFavorite).toEqual(true);
await store.dispatch(updateIsFavorite({ id: TimelineId.test, isFavorite: false }));

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(refreshTimelines as unknown as jest.Mock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(selectTimelineById(store.getState(), TimelineId.test)).toEqual(
expect.objectContaining({
isFavorite: false,
savedObjectId: newSavedObjectId,
version: newVersion,
})
);
});

it('should show an error message when the call is unauthorized', async () => {
(persistFavorite as jest.Mock).mockResolvedValue({
data: {
persistFavorite: {
code: 403,
},
},
});

await store.dispatch(updateIsFavorite({ id: TimelineId.test, isFavorite: true }));

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(showCallOutUnauthorizedMsgMock).toHaveBeenCalled();
});

it('should show a generic error when the persistence throws', async () => {
const addDangerMock = jest.spyOn(kibanaMock.notifications.toasts, 'addDanger');
(persistFavorite as jest.Mock).mockImplementation(() => {
throw new Error();
});

await store.dispatch(updateIsFavorite({ id: TimelineId.test, isFavorite: true }));

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(addDangerMock).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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 { createMockStore, kibanaMock } from '../../../common/mock';
import { selectTimelineById } from '../selectors';
import { TimelineId } from '../../../../common/types/timeline';
import { persistNote } from '../../containers/notes/api';
import { refreshTimelines } from './helpers';

import {
startTimelineSaving,
endTimelineSaving,
showCallOutUnauthorizedMsg,
addNote,
addNoteToEvent,
} from '../actions';
import { updateNote } from '../../../common/store/app/actions';
import { createNote } from '../../components/notes/helpers';

jest.mock('../actions', () => {
const actual = jest.requireActual('../actions');
const endTLSaving = jest.fn((...args) => actual.endTimelineSaving(...args));
(endTLSaving as unknown as { match: Function }).match = () => false;
return {
...actual,
showCallOutUnauthorizedMsg: jest
.fn()
.mockImplementation((...args) => actual.showCallOutUnauthorizedMsg(...args)),
startTimelineSaving: jest
.fn()
.mockImplementation((...args) => actual.startTimelineSaving(...args)),
endTimelineSaving: endTLSaving,
};
});
jest.mock('../../containers/notes/api');
jest.mock('./helpers');

const startTimelineSavingMock = startTimelineSaving as unknown as jest.Mock;
const endTimelineSavingMock = endTimelineSaving as unknown as jest.Mock;
const showCallOutUnauthorizedMsgMock = showCallOutUnauthorizedMsg as unknown as jest.Mock;

describe('Timeline note middleware', () => {
let store = createMockStore(undefined, undefined, kibanaMock);
const testNote = createNote({ newNote: 'test', user: 'elastic' });
const testEventId = 'test';

beforeEach(() => {
store = createMockStore(undefined, undefined, kibanaMock);
jest.clearAllMocks();
});

it('should persist a timeline note', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
},
},
},
});
expect(selectTimelineById(store.getState(), TimelineId.test).noteIds).toEqual([]);
await store.dispatch(updateNote({ note: testNote }));
await store.dispatch(addNote({ id: TimelineId.test, noteId: testNote.id }));

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(refreshTimelines as unknown as jest.Mock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(selectTimelineById(store.getState(), TimelineId.test).noteIds).toContain(testNote.id);
});

it('should persist a note on an event of a timeline', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 200,
message: 'success',
note: {
noteId: testNote.id,
},
},
},
});
expect(selectTimelineById(store.getState(), TimelineId.test).eventIdToNoteIds).toEqual({});
await store.dispatch(updateNote({ note: testNote }));
await store.dispatch(
addNoteToEvent({ eventId: testEventId, id: TimelineId.test, noteId: testNote.id })
);

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(refreshTimelines as unknown as jest.Mock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(selectTimelineById(store.getState(), TimelineId.test).eventIdToNoteIds).toEqual(
expect.objectContaining({
[testEventId]: [testNote.id],
})
);
});

it('should show an error message when the call is unauthorized', async () => {
(persistNote as jest.Mock).mockResolvedValue({
data: {
persistNote: {
code: 403,
},
},
});

await store.dispatch(updateNote({ note: testNote }));
await store.dispatch(addNote({ id: TimelineId.test, noteId: testNote.id }));

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(showCallOutUnauthorizedMsgMock).toHaveBeenCalled();
});

it('should show a generic error when the persistence throws', async () => {
const addDangerMock = jest.spyOn(kibanaMock.notifications.toasts, 'addDanger');
(persistNote as jest.Mock).mockImplementation(() => {
throw new Error();
});

await store.dispatch(updateNote({ note: testNote }));
await store.dispatch(addNote({ id: TimelineId.test, noteId: testNote.id }));

expect(startTimelineSavingMock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(addDangerMock).toHaveBeenCalled();
});
});
Loading

0 comments on commit 8f39000

Please sign in to comment.