From d784a03617dfe14ccc2bbd05f2edbfe2a7b68010 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 12:22:23 -0400 Subject: [PATCH 01/29] resolver simulator and click through tests --- .../resolver/data_access_layer/factory.ts | 58 +++ .../mocks/one_ancestor_two_children.ts | 75 ++++ .../public/resolver/models/process_event.ts | 3 +- .../resolver/models/simulator/index.tsx | 192 +++++++++ .../public/resolver/store/index.ts | 8 +- .../public/resolver/store/middleware/index.ts | 23 +- .../store/middleware/resolver_tree_fetcher.ts | 31 +- .../resolver/store/mocks/endpoint_event.ts | 2 +- .../connect_enzyme_wrapper_and_store.ts | 15 + .../resolver/test_utilities/extend_jest.ts | 86 ++++ .../resolver/test_utilities/spy_middleware.ts | 51 +++ .../public/resolver/types.ts | 87 ++++- .../resolver/view/clickthrough.test.tsx | 95 +++++ .../public/resolver/view/edge_line.tsx | 2 +- .../public/resolver/view/index.tsx | 45 +-- .../public/resolver/view/mock.tsx | 112 ++++++ .../panels/panel_content_process_detail.tsx | 368 +++++++++--------- .../view/panels/panel_content_utilities.tsx | 6 + .../resolver/view/process_event_dot.tsx | 11 +- .../view/resolver_without_providers.tsx | 149 +++++++ .../public/resolver/view/submenu.tsx | 2 +- .../public/resolver/view/use_camera.test.tsx | 5 +- .../view/use_resolver_query_params.ts | 4 +- 23 files changed, 1156 insertions(+), 274 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/models/simulator/index.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/mock.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts new file mode 100644 index 00000000000000..4a1c57566de725 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -0,0 +1,58 @@ +/* + * 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 { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../types'; +import { DataAccessLayer } from '../types'; +import { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../common/endpoint/types'; +import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants'; + +/** + * The only concrete DataAccessLayer. This isn't built in to Resolver. Instead we inject it. This way, tests can provide a fake one. + */ +export function dataAccessLayerFactory( + context: KibanaReactContextValue +): DataAccessLayer { + const dataAccessLayer: DataAccessLayer = { + async relatedEvents(entityID: string): Promise { + return context.services.http.get(`/api/endpoint/resolver/${entityID}/events`, { + query: { events: 100 }, + }); + }, + async resolverTree(entityID: string, signal: AbortSignal): Promise { + return context.services.http.get(`/api/endpoint/resolver/${entityID}`, { + signal, + }); + }, + + indexPatterns(): string[] { + return context.services.uiSettings.get(defaultIndexKey); + }, + + async entities({ + _id, + indices, + signal, + }: { + _id: string; + indices: string[]; + signal: AbortSignal; + }): Promise { + return context.services.http.get('/api/endpoint/resolver/entity', { + signal, + query: { + _id, + indices, + }, + }); + }, + }; + return dataAccessLayer; +} diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts new file mode 100644 index 00000000000000..025cc04d5f4686 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts @@ -0,0 +1,75 @@ +/* + * 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 { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; +import { mockEndpointEvent } from '../../store/mocks/endpoint_event'; +import { mockTreeWithNoAncestorsAnd2Children } from '../../store/mocks/resolver_tree'; +import { DataAccessLayer } from '../../types'; + +interface Metadata { + entityIDs: { origin: 'origin'; firstChild: 'firstChild'; secondChild: 'secondChild' }; +} + +/** + * Simplest mock dataAccessLayer possible. + */ +export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } { + const metadata: Metadata = { + entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, + }; + return { + metadata, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + relatedEvents(entityID: string): Promise { + return Promise.resolve({ + entityID, + events: [ + mockEndpointEvent({ + entityID, + name: 'event', + timestamp: 0, + }), + ], + nextEvent: null, + }); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + resolverTree(): Promise { + return Promise.resolve( + mockTreeWithNoAncestorsAnd2Children({ + originID: metadata.entityIDs.origin, + firstChildID: metadata.entityIDs.firstChild, + secondChildID: metadata.entityIDs.secondChild, + }) + ); + }, + + /** + * Get an array of index patterns that contain events. + */ + indexPatterns(): string[] { + return ['index pattern']; + }, + + /** + * Get entities matching a document. + */ + entities(): Promise { + return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index 4f8df87b3ac0b6..c00eafcda0e73c 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -144,7 +144,8 @@ export function processPath(passedEvent: ResolverEvent): string | undefined { */ export function userInfoForProcess( passedEvent: ResolverEvent -): { user?: string; domain?: string } | undefined { + // TODO, fix in 7.9 +): { name?: string; domain?: string } | undefined { return passedEvent.user; } diff --git a/x-pack/plugins/security_solution/public/resolver/models/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/models/simulator/index.tsx new file mode 100644 index 00000000000000..ca8f79ea108d7f --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/simulator/index.tsx @@ -0,0 +1,192 @@ +/* + * 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 React from 'react'; +import { Store, createStore, applyMiddleware } from 'redux'; +import { mount, ReactWrapper } from 'enzyme'; +import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; +import { connectEnzymeWrapperAndStore } from '../../test_utilities/connect_enzyme_wrapper_and_store'; +import { spyMiddlewareFactory } from '../../test_utilities/spy_middleware'; +import { resolverMiddlewareFactory } from '../../store/middleware'; +import { resolverReducer } from '../../store/reducer'; +import { MockResolver } from '../../view/mock'; +import { ResolverState, DataAccessLayer, SpyMiddleware } from '../../types'; +import { ResolverAction } from '../../store/actions'; + +export class Simulator { + private readonly store: Store; + private readonly history: HistoryPackageHistoryInterface; + private readonly wrapper: ReactWrapper; + private readonly spyMiddleware: SpyMiddleware; + constructor( + dataAccessLayer: DataAccessLayer, + private readonly resolverComponentInstanceID: string + ) { + this.spyMiddleware = spyMiddlewareFactory(); + + const middlewareEnhancer = applyMiddleware( + resolverMiddlewareFactory(dataAccessLayer), + this.spyMiddleware.middleware + ); + + this.store = createStore(resolverReducer, middlewareEnhancer); + + this.history = createMemoryHistory(); + + // Render Resolver via the `MockResolver` component, using `enzyme`. + this.wrapper = mount( + + ); + + // Update the enzyme wrapper after each state transition + connectEnzymeWrapperAndStore(this.store, this.wrapper); + } + + public debugActions(): () => void { + return this.spyMiddleware.debugActions(); + } + + /** + * Return a promise that resolves after the `store`'s next state transition. + */ + public stateTransitioned(): Promise { + let resolveState: (() => void) | null = null; + const promise: Promise = new Promise((resolve) => { + resolveState = resolve; + }); + const unsubscribe = this.store.subscribe(() => { + unsubscribe(); + resolveState!(); + }); + return promise; + } + + /** + * This will yield the return value of `mapper` after each state transition. If no state transition occurs for 10 event loops in a row, this will give up. + */ + public async *mapStateTransisions(mapper: () => R): AsyncIterable { + yield mapper(); + let timeoutCount = 0; + while (true) { + const maybeValue: { value: R; timedOut: false } | { timedOut: true } = await Promise.race([ + (async (): Promise<{ value: R; timedOut: false }> => { + await this.stateTransitioned(); + return { + value: mapper(), + timedOut: false, + }; + })(), + new Promise<{ timedOut: true }>((resolve) => { + setTimeout(() => { + return resolve({ timedOut: true }); + }, 0); + }), + ]); + if (maybeValue.timedOut) { + timeoutCount++; + if (timeoutCount === 10) { + return; + } + } else { + timeoutCount = 0; + yield mapper(); + } + } + } + + /** + * Find a process node element. Takes options supported by `resolverNodeSelector`. + * returns a `ReactWrapper` even if nothing is found, as that is how `enzyme` does things. + */ + public processNodeElements(options: ProcessNodeElementSelectorOptions = {}): ReactWrapper { + return this.findInDOM(processNodeElementSelector(options)); + } + + /** + * true if a process node element is found for the entityID and if it has an [aria-selected] attribute. + */ + public processNodeElementLooksSelected(entityID: string): boolean { + return this.processNodeElements({ entityID, selected: true }).length === 1; + } + + /** + * true if a process node element is found for the entityID and if it has an [aria-selected] attribute. + */ + public processNodeElementLooksUnselected(entityID: string): boolean { + // find the process node, then exclude it if its selected. + return ( + this.processNodeElements({ entityID }).not( + processNodeElementSelector({ entityID, selected: true }) + ).length === 1 + ); + } + + /** + * Given a `History` and a `resolverDocumentID`, return any values stored in the query string. + * This isn't exactly the same as the query string state, because parsing that from the query string + * would be business logic. For example, this doesn't ignore duplicates. + * Use this for testing. + */ + public queryStringValues(): { selectedNode: string[] } { + const urlSearchParams = new URLSearchParams(this.history.location.search); + return { + selectedNode: urlSearchParams.getAll(`resolver-${this.resolverComponentInstanceID}-id`), + }; + } + + /** + * The element that shows when Resolver is waiting for the graph data. + */ + public graphLoadingElement(): ReactWrapper { + return this.findInDOM('[data-test-subj="resolver:graph:loading"]'); + } + + /** + * The element that shows if Resolver couldn't draw the graph. + */ + public graphErrorElement(): ReactWrapper { + return this.findInDOM('[data-test-subj="resolver:graph:error"]'); + } + + /** + * The element where nodes get drawn. + */ + public graphElement(): ReactWrapper { + return this.findInDOM('[data-test-subj="resolver:graph"]'); + } + + /** + * Like `this.wrapper.find` but only returns DOM nodes. + */ + public findInDOM(selector: string): ReactWrapper { + return this.wrapper.find(selector).filterWhere((wrapper) => typeof wrapper.type() === 'string'); + } +} + +const baseResolverSelector = '[data-test-subj="resolver:node"]'; + +interface ProcessNodeElementSelectorOptions { + entityID?: string; + selected?: boolean; +} + +function processNodeElementSelector({ + entityID, + selected = false, +}: ProcessNodeElementSelectorOptions = {}): string { + let selector: string = baseResolverSelector; + if (entityID !== undefined) { + selector += `[data-test-resolver-node-id="${entityID}"]`; + } + if (selected) { + selector += '[aria-selected="true"]'; + } + return selector; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/index.ts b/x-pack/plugins/security_solution/public/resolver/store/index.ts index d9e750241ced1f..950a61db33f177 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/index.ts @@ -6,22 +6,20 @@ import { createStore, applyMiddleware, Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; -import { KibanaReactContextValue } from '../../../../../../src/plugins/kibana_react/public'; -import { ResolverState } from '../types'; -import { StartServices } from '../../types'; +import { ResolverState, DataAccessLayer } from '../types'; import { resolverReducer } from './reducer'; import { resolverMiddlewareFactory } from './middleware'; import { ResolverAction } from './actions'; export const storeFactory = ( - context?: KibanaReactContextValue + dataAccessLayer: DataAccessLayer ): Store => { const actionsDenylist: Array = ['userMovedPointer']; const composeEnhancers = composeWithDevTools({ name: 'Resolver', actionsBlacklist: actionsDenylist, }); - const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(context)); + const middlewareEnhancer = applyMiddleware(resolverMiddlewareFactory(dataAccessLayer)); return createStore(resolverReducer, composeEnhancers(middlewareEnhancer)); }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index 398e855a1f5d40..4ddfac12c11940 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -5,15 +5,13 @@ */ import { Dispatch, MiddlewareAPI } from 'redux'; -import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; -import { StartServices } from '../../../types'; -import { ResolverState } from '../../types'; +import { ResolverState, DataAccessLayer } from '../../types'; import { ResolverRelatedEvents } from '../../../../common/endpoint/types'; import { ResolverTreeFetcher } from './resolver_tree_fetcher'; import { ResolverAction } from '../actions'; type MiddlewareFactory = ( - context?: KibanaReactContextValue + dataAccessLayer: DataAccessLayer ) => ( api: MiddlewareAPI, S> ) => (next: Dispatch) => (action: ResolverAction) => unknown; @@ -24,15 +22,9 @@ type MiddlewareFactory = ( * For actions that the app triggers directly, use `app` as a prefix for the type. * For actions that are triggered as a result of server interaction, use `server` as a prefix for the type. */ -export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { +export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: DataAccessLayer) => { return (api) => (next) => { - // This cannot work w/o `context`. - if (!context) { - return async (action: ResolverAction) => { - next(action); - }; - } - const resolverTreeFetcher = ResolverTreeFetcher(context, api); + const resolverTreeFetcher = ResolverTreeFetcher(dataAccessLayer, api); return async (action: ResolverAction) => { next(action); @@ -45,12 +37,7 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { const entityIdToFetchFor = action.payload; let result: ResolverRelatedEvents | undefined; try { - result = await context.services.http.get( - `/api/endpoint/resolver/${entityIdToFetchFor}/events`, - { - query: { events: 100 }, - } - ); + result = await dataAccessLayer.relatedEvents(entityIdToFetchFor); } catch { api.dispatch({ type: 'serverFailedToReturnRelatedEventData', diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts index 7d16dc251e6fc9..2c98059d420e8f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts @@ -9,11 +9,8 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { ResolverTree, ResolverEntityIndex } from '../../../../common/endpoint/types'; -import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; -import { ResolverState } from '../../types'; +import { ResolverState, DataAccessLayer } from '../../types'; import * as selectors from '../selectors'; -import { StartServices } from '../../../types'; -import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../../common/constants'; import { ResolverAction } from '../actions'; /** * A function that handles syncing ResolverTree data w/ the current entity ID. @@ -23,7 +20,7 @@ import { ResolverAction } from '../actions'; * This is a factory because it is stateful and keeps that state in closure. */ export function ResolverTreeFetcher( - context: KibanaReactContextValue, + dataAccessLayer: DataAccessLayer, api: MiddlewareAPI, ResolverState> ): () => void { let lastRequestAbortController: AbortController | undefined; @@ -48,17 +45,12 @@ export function ResolverTreeFetcher( payload: databaseDocumentIDToFetch, }); try { - const indices: string[] = context.services.uiSettings.get(defaultIndexKey); - const matchingEntities: ResolverEntityIndex = await context.services.http.get( - '/api/endpoint/resolver/entity', - { - signal: lastRequestAbortController.signal, - query: { - _id: databaseDocumentIDToFetch, - indices, - }, - } - ); + const indices: string[] = dataAccessLayer.indexPatterns(); + const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({ + _id: databaseDocumentIDToFetch, + indices, + signal: lastRequestAbortController.signal, + }); if (matchingEntities.length < 1) { // If no entity_id could be found for the _id, bail out with a failure. api.dispatch({ @@ -68,9 +60,10 @@ export function ResolverTreeFetcher( return; } const entityIDToFetch = matchingEntities[0].entity_id; - result = await context.services.http.get(`/api/endpoint/resolver/${entityIDToFetch}`, { - signal: lastRequestAbortController.signal, - }); + result = await dataAccessLayer.resolverTree( + entityIDToFetch, + lastRequestAbortController.signal + ); } catch (error) { // https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-AbortError if (error instanceof DOMException && error.name === 'AbortError') { diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts index 8f2e0ad3a6d858..709f2faf13b006 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/endpoint_event.ts @@ -18,7 +18,7 @@ export function mockEndpointEvent({ }: { entityID: string; name: string; - parentEntityId: string | undefined; + parentEntityId?: string; timestamp: number; lifecycleType?: string; }): EndpointEvent { diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts new file mode 100644 index 00000000000000..9be69bd9d4a705 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts @@ -0,0 +1,15 @@ +/* + * 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 { Store } from 'redux'; +import { ReactWrapper } from 'enzyme'; + +export function connectEnzymeWrapperAndStore(store: Store, wrapper: ReactWrapper): void { + store.subscribe(() => { + // update the enzyme wrapper after each state transition + return wrapper.update(); + }); +} diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts new file mode 100644 index 00000000000000..87bbb21e9a46fa --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -0,0 +1,86 @@ +/* + * 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. + */ + +/** + * Typescript won't allow global namespace stuff unless you're in a module. + * This wouldn't otherwise be a module. The code runs as soon as it's imported. + * This is done this way because the `declare` will be active on import, so in + * order to be correct, the code that the `declare` declares needs to be available on import as well. + */ +export {}; +declare global { + /* eslint-disable @typescript-eslint/no-namespace */ + namespace jest { + interface Matchers { + // Type the custom matcher + toSometimesYieldEqualTo(b: T): Promise; + } + } +} + +expect.extend({ + /** + * A custom matcher that takes an async generator and compares each value it yields to an expected value. + * If any yielded value deep equals the expected value, the matcher will pass. + * If the generator ends with none of the yielded values matching, it will fail. + */ + async toSometimesYieldEqualTo( + this: jest.MatcherContext, + receivedIterable: AsyncIterable, + expected: T + ): Promise<{ pass: boolean; message: () => string }> { + // Used in printing out the pass or fail message + const matcherName = 'toSometimesYieldEqualTo'; + const options = { + comment: 'deep equality with any yielded value', + isNot: this.isNot, + promise: this.promise, + }; + // The last value received: Used in printing the message + let lastReceived: T | undefined; + + // Set to true if the test passes. + let pass: boolean = false; + + // Aysync iterate over the iterable + for await (const received of receivedIterable) { + // keep track of the last value. Used in both pass and fail messages + lastReceived = received; + // Use deep equals to compare the value to the expected value + if (this.equals(received, expected)) { + // If the value is equal, break + pass = true; + break; + } + } + + // Use `pass` as set in the above loop (or initialized to `false`) + // See https://jestjs.io/docs/en/expect#custom-matchers-api and https://jestjs.io/docs/en/expect#thisutils + const message = pass + ? () => + `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + + `Expected: not ${this.utils.printExpected(expected)}\n${ + this.utils.stringify(expected) !== this.utils.stringify(lastReceived!) + ? `Received: ${this.utils.printReceived(lastReceived)}` + : '' + }` + : () => + `${this.utils.matcherHint( + matcherName, + undefined, + undefined, + options + )}\n\n${this.utils.printDiffOrStringify( + expected, + lastReceived, + 'Expected', + 'Received', + this.expand + )}`; + + return { message, pass }; + }, +}); diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware.ts new file mode 100644 index 00000000000000..7878e6849034e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware.ts @@ -0,0 +1,51 @@ +/* + * 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 { ResolverAction } from '../store/actions'; +import { SpyMiddleware, SpyMiddlewareStateActionPair } from '../types'; + +// TODO, rename file +export const spyMiddlewareFactory: () => SpyMiddleware = () => { + const resolvers: Set<(stateActionPair: SpyMiddlewareStateActionPair) => void> = new Set(); + + const actions = async function* actions() { + while (true) { + const promise: Promise = new Promise((resolve) => { + resolvers.add(resolve); + }); + yield await promise; + } + }; + + return { + middleware: (api) => (next) => (action: ResolverAction) => { + const state = api.getState(); + const oldResolvers = [...resolvers]; + resolvers.clear(); + for (const resolve of oldResolvers) { + resolve({ action, state }); + } + + next(action); + }, + actions, + debugActions() { + let stop: boolean = false; + (async () => { + for await (const action of actions()) { + if (stop) { + break; + } + // eslint-disable-next-line no-console + console.log('action', action); + } + })(); + return () => { + stop = true; + }; + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 02a890ca13ee8e..80d99109eae39c 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -4,10 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable no-duplicate-imports */ + import { Store } from 'redux'; +import { Middleware, Dispatch } from 'redux'; import { BBox } from 'rbush'; import { ResolverAction } from './store/actions'; -import { ResolverEvent, ResolverRelatedEvents, ResolverTree } from '../../common/endpoint/types'; +import { + ResolverEvent, + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../common/endpoint/types'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -436,3 +444,80 @@ export interface IsometricTaxiLayout { */ ariaLevels: Map; } + +/** + * An object with methods that can be used to access data from the Kibana server. + * This is injected into Resolver. + * This allows tests to provide a mock data access layer. + * In the future, other implementations of Resolver could provide different data access layers. + */ +export interface DataAccessLayer { + /** + * Fetch related events for an entity ID + */ + relatedEvents: (entityID: string) => Promise; + + /** + * Fetch a ResolverTree for a entityID + */ + resolverTree: (entityID: string, signal: AbortSignal) => Promise; + + /** + * Get an array of index patterns that contain events. + */ + indexPatterns: () => string[]; + + /** + * Get entities matching a document. + */ + entities: (parameters: { + /** _id of the document to find an entity in. */ + _id: string; + /** indices to search in */ + indices: string[]; + /** signal to abort the request */ + signal: AbortSignal; + }) => Promise; +} + +/** + * The externally provided React props. + */ +export interface ResolverProps { + /** + * Used by `styled-components`. + */ + className?: string; + /** + * The `_id` value of an event in ES. + * Used as the origin of the Resolver graph. + */ + databaseDocumentID?: string; + /** + * A string literal describing where in the app resolver is located, + * used to prevent collisions in things like query params + */ + resolverComponentInstanceID: string; +} + +export interface SpyMiddlewareStateActionPair { + action: ResolverAction; + state: ResolverState; +} + +export interface SpyMiddleware { + /** + * A middleware to use with `applyMiddleware`. + */ + middleware: Middleware<{}, ResolverState, Dispatch>; + /** + * A generator that returns all state and action pairs that pass through the middleware. + */ + actions: () => AsyncGenerator; + + /** + * Prints actions to the console. + * Call the returned function to stop debugging. + */ + debugActions: () => () => void; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx new file mode 100644 index 00000000000000..ca99d8f2e6999d --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { oneAncestorTwoChildren } from '../data_access_layer/mocks/one_ancestor_two_children'; +import { Simulator } from '../models/simulator'; +// Extend jest with a custom matcher +import '../test_utilities/extend_jest'; + +describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', () => { + let simulator: Simulator; + let entityIDs: { origin: string; firstChild: string; secondChild: string }; + + // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances + const resolverComponentInstanceID = 'resolverComponentInstanceID'; + + beforeEach(async () => { + // create a mock data access layer + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = oneAncestorTwoChildren(); + + // save a reference to the entity IDs exposed by the mock data layer + entityIDs = dataAccessLayerMetadata.entityIDs; + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = new Simulator(dataAccessLayer, resolverComponentInstanceID); + }); + + describe('when it has loaded', () => { + beforeEach(async () => { + await expect( + /** + * It's important that all of these are done in a single `expect`. + * If you do them concurrently with each other, you'll have incorrect results. + * + * For example, there might be no loading element at one point, and 1 graph element at one point, but never a single time when there is both 1 graph element and 0 loading elements. + */ + simulator.mapStateTransisions(() => ({ + graphElements: simulator.graphElement().length, + graphLoadingElements: simulator.graphLoadingElement().length, + graphErrorElements: simulator.graphErrorElement().length, + })) + ).toSometimesYieldEqualTo({ + // it should have 1 graph element, an no error or loading elements. + graphElements: 1, + graphLoadingElements: 0, + graphErrorElements: 0, + }); + }); + + // Combining assertions here for performance. Unfortunately, Enzyme + jsdom + React is slow. + it(`should have 3 nodes, with the entityID's 'origin', 'firstChild', and 'secondChild'. 'origin' should be selected.`, async () => { + expect(simulator.processNodeElementLooksSelected(entityIDs.origin)).toBe(true); + + expect(simulator.processNodeElementLooksUnselected(entityIDs.firstChild)).toBe(true); + expect(simulator.processNodeElementLooksUnselected(entityIDs.secondChild)).toBe(true); + + expect(simulator.processNodeElements().length).toBe(3); + }); + + describe("when the second child node's first button has been clicked", () => { + beforeEach(() => { + // Click the first button under the second child element. + simulator + .processNodeElements({ entityID: entityIDs.secondChild }) + .find('button') + .simulate('click'); + }); + it('should render the second child node as selected, and the first child not as not selected, and the query string should indicate that the second child is selected', async () => { + await expect( + simulator.mapStateTransisions(function value() { + return { + // the query string has a key showing that the second child is selected + queryStringSelectedNode: simulator.queryStringValues().selectedNode, + // the second child is rendered in the DOM, and shows up as selected + secondChildLooksSelected: simulator.processNodeElementLooksSelected( + entityIDs.secondChild + ), + // the origin is in the DOM, but shows up as unselected + originLooksUnselected: simulator.processNodeElementLooksUnselected(entityIDs.origin), + }; + }) + ).toSometimesYieldEqualTo({ + // Just the second child should be marked as selected in the query string + queryStringSelectedNode: [entityIDs.secondChild], + // The second child is rendered and has `[aria-selected]` + secondChildLooksSelected: true, + // The origin child is rendered and doesn't have `[aria-selected]` + originLooksUnselected: true, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 9f310bb1cc0d65..65c70f94432c79 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -45,7 +45,7 @@ const StyledElapsedTime = styled.div` left: ${(props) => `${props.leftPct}%`}; padding: 6px 8px; border-radius: 999px; // generate pill shape - transform: translate(-50%, -50%); + transform: translate(-50%, -50%) rotateX(35deg); user-select: none; `; diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index c1ffa42d02abbc..cf96fae40aa31e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -7,50 +7,29 @@ import React, { useMemo } from 'react'; import { Provider } from 'react-redux'; -import { ResolverMap } from './map'; import { storeFactory } from '../store'; import { StartServices } from '../../types'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { DataAccessLayer, ResolverProps } from '../types'; +import { dataAccessLayerFactory } from '../data_access_layer/factory'; +import { ResolverWithoutProviders } from './resolver_without_providers'; /** - * The top level, unconnected, Resolver component. + * The `Resolver` component to use. This sets up the DataAccessLayer provider. Use `ResolverWithoutStore` in tests or in other scenarios where you want to provide a different (or fake) data access layer. */ -export const Resolver = React.memo(function ({ - className, - databaseDocumentID, - resolverComponentInstanceID, -}: { - /** - * Used by `styled-components`. - */ - className?: string; - /** - * The `_id` value of an event in ES. - * Used as the origin of the Resolver graph. - */ - databaseDocumentID?: string; - /** - * A string literal describing where in the app resolver is located, - * used to prevent collisions in things like query params - */ - resolverComponentInstanceID: string; -}) { +export const Resolver = React.memo((props: ResolverProps) => { const context = useKibana(); + const dataAccessLayer: DataAccessLayer = useMemo(() => dataAccessLayerFactory(context), [ + context, + ]); + const store = useMemo(() => { - return storeFactory(context); - }, [context]); + return storeFactory(dataAccessLayer); + }, [dataAccessLayer]); - /** - * Setup the store and use `Provider` here. This allows the ResolverMap component to - * dispatch actions and read from state. - */ return ( - + ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/mock.tsx b/x-pack/plugins/security_solution/public/resolver/view/mock.tsx new file mode 100644 index 00000000000000..d3e65e3d86e38b --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/mock.tsx @@ -0,0 +1,112 @@ +/* + * 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. + */ + +// TODO, this is really part of the simulator. move it + +/* eslint-disable no-duplicate-imports */ + +/* eslint-disable react/display-name */ + +import React, { useMemo, useEffect, useState, useCallback } from 'react'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { I18nProvider } from '@kbn/i18n/react'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { CoreStart } from '../../../../../../src/core/public'; +import { ResolverState, SideEffectSimulator, ResolverProps } from '../types'; +import { ResolverAction } from '../store/actions'; +import { ResolverWithoutProviders } from './resolver_without_providers'; +import { SideEffectContext } from './side_effect_context'; +import { sideEffectSimulator } from './side_effect_simulator'; + +type MockResolverProps = { + // core start and history can be optionally passed + coreStart?: CoreStart; + history?: React.ComponentProps['history']; + // If passed, set the raster width to this value. Defaults to 800 + rasterWidth?: number; + // If passed, set the raster height to this value. Defaults to 800 + rasterHeight?: number; + /** Pass a resolver store. See `storeFactory` and `mockDataAccessLayer` */ + store: Store; + // All the props from `ResolverWithoutStore` can be optionally passed. +} & Partial; + +/** + * This is a mock Resolver component. It has faked versions of various services: + * * fake i18n + * * fake (memory) history (optionally provided) + * * fake coreStart services (optionally provided) + * * SideEffectContext + * + * You will need to provide a store. Create one with `storyFactory`. The store will need a mock `DataAccessLayer`. + * + * Props required by `ResolverWithoutStore` can be passed as well. If not passed, they are defaulted. + * * `databaseDocumentID` + * * `resolverComponentInstanceID` + * + * Use this in jest tests. Render it w/ `@testing-library/react` or `enzyme`. Then either interact with the result using fake events, or dispatch actions to the store. You could also pass in a store with initial data. + */ +export const MockResolver = React.memo((props: MockResolverProps) => { + // Get the coreStart services from props, or create them if needed. + const coreStart: CoreStart = useMemo(() => props.coreStart ?? coreMock.createStart(), [ + props.coreStart, + ]); + + // Get the history object from props, or create it if needed. + const history = useMemo(() => props.history ?? createMemoryHistory(), [props.history]); + + const [resolverElement, setResolverElement] = useState(null); + + // Get a ref to the underlying Resolver element so we can resize. + // Use a callback function because the underlying DOM node can change. In fact, `enzyme` seems to change it a lot. + const resolverRef = useCallback((element: HTMLDivElement | null) => { + setResolverElement(element); + }, []); + + const simulator: SideEffectSimulator = useMemo(() => sideEffectSimulator(), []); + + // Resize the Resolver element to match the passed in props. Resolver is size dependent. + useEffect(() => { + if (resolverElement) { + const size: DOMRect = { + width: props.rasterWidth ?? 1600, + height: props.rasterHeight ?? 1200, + x: 0, + y: 0, + bottom: 0, + left: 0, + top: 0, + right: 0, + toJSON() { + return this; + }, + }; + simulator.controls.simulateElementResize(resolverElement, size); + } + }, [props.rasterWidth, props.rasterHeight, simulator.controls, resolverElement]); + + return ( + + + + + + + + + + + + ); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx index 29c7676d2167de..bdaba3f08575a8 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx @@ -1,184 +1,184 @@ -/* - * 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 React, { memo, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { i18n } from '@kbn/i18n'; -import { - htmlIdGenerator, - EuiSpacer, - EuiTitle, - EuiText, - EuiTextColor, - EuiDescriptionList, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { FormattedMessage } from 'react-intl'; -import * as selectors from '../../store/selectors'; -import * as event from '../../../../common/endpoint/models/event'; -import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities'; -import { - processPath, - processPid, - userInfoForProcess, - processParentPid, - md5HashForProcess, - argsForProcess, -} from '../../models/process_event'; -import { CubeForProcess } from './process_cube_icon'; -import { ResolverEvent } from '../../../../common/endpoint/types'; -import { useResolverTheme } from '../assets'; - -const StyledDescriptionList = styled(EuiDescriptionList)` - &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { - max-width: 10em; - } -`; - -/** - * A description list view of all the Metadata that goes with a particular process event, like: - * Created, Pid, User/Domain, etc. - */ -export const ProcessDetails = memo(function ProcessDetails({ - processEvent, - pushToQueryParams, -}: { - processEvent: ResolverEvent; - pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; -}) { - const processName = event.eventName(processEvent); - const entityId = event.entityId(processEvent); - const isProcessTerminated = useSelector(selectors.isProcessTerminated)(entityId); - const processInfoEntry = useMemo(() => { - const eventTime = event.eventTimestamp(processEvent); - const dateTime = eventTime ? formatDate(eventTime) : ''; - - const createdEntry = { - title: '@timestamp', - description: dateTime, - }; - - const pathEntry = { - title: 'process.executable', - description: processPath(processEvent), - }; - - const pidEntry = { - title: 'process.pid', - description: processPid(processEvent), - }; - - const userEntry = { - title: 'user.name', - description: (userInfoForProcess(processEvent) as { name: string }).name, - }; - - const domainEntry = { - title: 'user.domain', - description: (userInfoForProcess(processEvent) as { domain: string }).domain, - }; - - const parentPidEntry = { - title: 'process.parent.pid', - description: processParentPid(processEvent), - }; - - const md5Entry = { - title: 'process.hash.md5', - description: md5HashForProcess(processEvent), - }; - - const commandLineEntry = { - title: 'process.args', - description: argsForProcess(processEvent), - }; - - // This is the data in {title, description} form for the EUIDescriptionList to display - const processDescriptionListData = [ - createdEntry, - pathEntry, - pidEntry, - userEntry, - domainEntry, - parentPidEntry, - md5Entry, - commandLineEntry, - ] - .filter((entry) => { - return entry.description; - }) - .map((entry) => { - return { - ...entry, - description: String(entry.description), - }; - }); - - return processDescriptionListData; - }, [processEvent]); - - const crumbs = useMemo(() => { - return [ - { - text: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processDescList.events', - { - defaultMessage: 'Events', - } - ), - onClick: () => { - pushToQueryParams({ crumbId: '', crumbEvent: '' }); - }, - }, - { - text: ( - <> - - - ), - onClick: () => {}, - }, - ]; - }, [processName, pushToQueryParams]); - const { cubeAssetsForNode } = useResolverTheme(); - const { descriptionText } = useMemo(() => { - if (!processEvent) { - return { descriptionText: '' }; - } - return cubeAssetsForNode(isProcessTerminated, false); - }, [processEvent, cubeAssetsForNode, isProcessTerminated]); - - const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []); - return ( - <> - - - -

- - {processName} -

-
- - - {descriptionText} - - - - - - ); -}); -ProcessDetails.displayName = 'ProcessDetails'; +/* + * 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 React, { memo, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { + htmlIdGenerator, + EuiSpacer, + EuiTitle, + EuiText, + EuiTextColor, + EuiDescriptionList, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { FormattedMessage } from 'react-intl'; +import * as selectors from '../../store/selectors'; +import * as event from '../../../../common/endpoint/models/event'; +import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities'; +import { + processPath, + processPid, + userInfoForProcess, + processParentPid, + md5HashForProcess, + argsForProcess, +} from '../../models/process_event'; +import { CubeForProcess } from './process_cube_icon'; +import { ResolverEvent } from '../../../../common/endpoint/types'; +import { useResolverTheme } from '../assets'; + +const StyledDescriptionList = styled(EuiDescriptionList)` + &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { + max-width: 10em; + } +`; + +/** + * A description list view of all the Metadata that goes with a particular process event, like: + * Created, Pid, User/Domain, etc. + */ +export const ProcessDetails = memo(function ProcessDetails({ + processEvent, + pushToQueryParams, +}: { + processEvent: ResolverEvent; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; +}) { + const processName = event.eventName(processEvent); + const entityId = event.entityId(processEvent); + const isProcessTerminated = useSelector(selectors.isProcessTerminated)(entityId); + const processInfoEntry = useMemo(() => { + const eventTime = event.eventTimestamp(processEvent); + const dateTime = eventTime ? formatDate(eventTime) : ''; + + const createdEntry = { + title: '@timestamp', + description: dateTime, + }; + + const pathEntry = { + title: 'process.executable', + description: processPath(processEvent), + }; + + const pidEntry = { + title: 'process.pid', + description: processPid(processEvent), + }; + + const userEntry = { + title: 'user.name', + description: userInfoForProcess(processEvent)?.name, + }; + + const domainEntry = { + title: 'user.domain', + description: userInfoForProcess(processEvent)?.domain, + }; + + const parentPidEntry = { + title: 'process.parent.pid', + description: processParentPid(processEvent), + }; + + const md5Entry = { + title: 'process.hash.md5', + description: md5HashForProcess(processEvent), + }; + + const commandLineEntry = { + title: 'process.args', + description: argsForProcess(processEvent), + }; + + // This is the data in {title, description} form for the EUIDescriptionList to display + const processDescriptionListData = [ + createdEntry, + pathEntry, + pidEntry, + userEntry, + domainEntry, + parentPidEntry, + md5Entry, + commandLineEntry, + ] + .filter((entry) => { + return entry.description; + }) + .map((entry) => { + return { + ...entry, + description: String(entry.description), + }; + }); + + return processDescriptionListData; + }, [processEvent]); + + const crumbs = useMemo(() => { + return [ + { + text: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.processDescList.events', + { + defaultMessage: 'Events', + } + ), + onClick: () => { + pushToQueryParams({ crumbId: '', crumbEvent: '' }); + }, + }, + { + text: ( + <> + + + ), + onClick: () => {}, + }, + ]; + }, [processName, pushToQueryParams]); + const { cubeAssetsForNode } = useResolverTheme(); + const { descriptionText } = useMemo(() => { + if (!processEvent) { + return { descriptionText: '' }; + } + return cubeAssetsForNode(isProcessTerminated, false); + }, [processEvent, cubeAssetsForNode, isProcessTerminated]); + + const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []); + return ( + <> + + + +

+ + {processName} +

+
+ + + {descriptionText} + + + + + + ); +}); +ProcessDetails.displayName = 'ProcessDetails'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 55b5be21fb4a45..9b68df46f69b6b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -27,7 +27,13 @@ const BetaHeader = styled(`header`)` * The two query parameters we read/write on to control which view the table presents: */ export interface CrumbInfo { + /** + * @deprecated + */ crumbId: string; + /** + * @deprecated + */ crumbEvent: string; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index aed292e4a39d16..0c7ddc9c108fb9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -195,7 +195,7 @@ const UnstyledProcessEventDot = React.memo( * `beginElement` is by [w3](https://www.w3.org/TR/SVG11/animate.html#__smil__ElementTimeControl__beginElement) * but missing in [TSJS-lib-generator](https://github.com/microsoft/TSJS-lib-generator/blob/15a4678e0ef6de308e79451503e444e9949ee849/inputfiles/addedTypes.json#L1819) */ - beginElement: () => void; + beginElement?: () => void; }) | null; } = React.createRef(); @@ -238,10 +238,8 @@ const UnstyledProcessEventDot = React.memo( const { pushToQueryParams } = useResolverQueryParams(); const handleClick = useCallback(() => { - if (animationTarget.current !== null) { - // This works but the types are missing in the typescript DOM lib - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (animationTarget.current as any).beginElement(); + if (animationTarget.current?.beginElement) { + animationTarget.current.beginElement(); } dispatch({ type: 'userSelectedResolverNode', @@ -297,7 +295,8 @@ const UnstyledProcessEventDot = React.memo( */ return (
{ + // Supply `useCamera` with the ref + cameraRef(element); + + // If a ref is being forwarded, populate that as well. + if (typeof refToForward === 'function') { + refToForward(element); + } else if (refToForward !== null) { + refToForward.current = element; + } + }, + [cameraRef, refToForward] + ); + const isLoading = useSelector(selectors.isLoading); + const hasError = useSelector(selectors.hasError); + const activeDescendantId = useSelector(selectors.ariaActiveDescendant); + const { colorMap } = useResolverTheme(); + const { + cleanUpQueryParams, + queryParams: { crumbId }, + pushToQueryParams, + } = useResolverQueryParams(); + + useEffectOnce(() => { + return () => cleanUpQueryParams(); + }); + + useEffect(() => { + // When you refresh the page after selecting a process in the table view (not the timeline view) + // The old crumbId still exists in the query string even though a resolver is no longer visible + // This just makes sure the activeDescendant and crumbId are in sync on load for that view as well as the timeline + if (activeDescendantId && crumbId !== activeDescendantId) { + pushToQueryParams({ crumbId: activeDescendantId, crumbEvent: '' }); + } + }, [crumbId, activeDescendantId, pushToQueryParams]); + + return ( + + {isLoading ? ( +
+ +
+ ) : hasError ? ( +
+
+ {' '} + +
+
+ ) : ( + + {connectingEdgeLineSegments.map( + ({ points: [startPosition, endPosition], metadata }) => ( + + ) + )} + {[...processNodePositions].map(([processEvent, position]) => { + const processEntityId = entityId(processEvent); + return ( + + ); + })} + + )} + + + +
+ ); + }) +); diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 6a9ab184e9bab0..2499a451b9c8c2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -190,7 +190,7 @@ const NodeSubMenuComponents = React.memo( * then force the popover to reposition itself. */ popoverRef.current && - projectionMatrixAtLastRender.current && + !projectionMatrixAtLastRender.current && projectionMatrixAtLastRender.current !== projectionMatrix ) { popoverRef.current.positionPopoverFixed(); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index a27f157bc93643..8ffe45edcfd8ac 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -10,7 +10,6 @@ import { renderHook, act as hooksAct } from '@testing-library/react-hooks'; import { useCamera, useAutoUpdatingClientRect } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; -import { storeFactory } from '../store'; import { Matrix3, ResolverStore, SideEffectSimulator } from '../types'; import { ResolverEvent } from '../../../common/endpoint/types'; import { SideEffectContext } from './side_effect_context'; @@ -19,6 +18,8 @@ import { sideEffectSimulator } from './side_effect_simulator'; import { mockProcessEvent } from '../models/process_event_test_helpers'; import { mock as mockResolverTree } from '../models/resolver_tree'; import { ResolverAction } from '../store/actions'; +import { createStore } from 'redux'; +import { resolverReducer } from '../store/reducer'; describe('useCamera on an unpainted element', () => { let element: HTMLElement; @@ -29,7 +30,7 @@ describe('useCamera on an unpainted element', () => { let simulator: SideEffectSimulator; beforeEach(async () => { - store = storeFactory(); + store = createStore(resolverReducer); const Test = function Test() { const camera = useCamera(); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts index 84d954de6ef274..5247e30c7749c3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts @@ -20,8 +20,8 @@ export function useResolverQueryParams() { const history = useHistory(); const urlSearch = useLocation().search; const resolverComponentInstanceID = useSelector(selectors.resolverComponentInstanceID); - const uniqueCrumbIdKey: string = `resolver-id:${resolverComponentInstanceID}`; - const uniqueCrumbEventKey: string = `resolver-event:${resolverComponentInstanceID}`; + const uniqueCrumbIdKey: string = `resolver-${resolverComponentInstanceID}-id`; + const uniqueCrumbEventKey: string = `resolver-${resolverComponentInstanceID}-event`; const pushToQueryParams = useCallback( (newCrumbs: CrumbInfo) => { // Construct a new set of params from the current set (minus empty params) From d63bca79dab2bc04a00b19d4f31b2f0f84619315 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 12:30:51 -0400 Subject: [PATCH 02/29] cleanup --- .../security_solution/public/resolver/models/process_event.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index c00eafcda0e73c..af465c77c4ba8c 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -144,7 +144,6 @@ export function processPath(passedEvent: ResolverEvent): string | undefined { */ export function userInfoForProcess( passedEvent: ResolverEvent - // TODO, fix in 7.9 ): { name?: string; domain?: string } | undefined { return passedEvent.user; } From 1d180eadb9d3e597b0ce7f33abb7c53b62985aa6 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 12:51:52 -0400 Subject: [PATCH 03/29] comments and code org --- .../simulator.tsx} | 101 +++++++++++++++--- .../resolver/view/clickthrough.test.tsx | 2 +- 2 files changed, 86 insertions(+), 17 deletions(-) rename x-pack/plugins/security_solution/public/resolver/{models/simulator/index.tsx => test_utilities/simulator.tsx} (55%) diff --git a/x-pack/plugins/security_solution/public/resolver/models/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx similarity index 55% rename from x-pack/plugins/security_solution/public/resolver/models/simulator/index.tsx rename to x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx index ca8f79ea108d7f..63d35e98e6059e 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx @@ -8,32 +8,60 @@ import React from 'react'; import { Store, createStore, applyMiddleware } from 'redux'; import { mount, ReactWrapper } from 'enzyme'; import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; -import { connectEnzymeWrapperAndStore } from '../../test_utilities/connect_enzyme_wrapper_and_store'; -import { spyMiddlewareFactory } from '../../test_utilities/spy_middleware'; -import { resolverMiddlewareFactory } from '../../store/middleware'; -import { resolverReducer } from '../../store/reducer'; -import { MockResolver } from '../../view/mock'; -import { ResolverState, DataAccessLayer, SpyMiddleware } from '../../types'; -import { ResolverAction } from '../../store/actions'; +import { connectEnzymeWrapperAndStore } from './connect_enzyme_wrapper_and_store'; +import { spyMiddlewareFactory } from './spy_middleware'; +import { resolverMiddlewareFactory } from '../store/middleware'; +import { resolverReducer } from '../store/reducer'; +import { MockResolver } from '../view/mock'; +import { ResolverState, DataAccessLayer, SpyMiddleware } from '../types'; +import { ResolverAction } from '../store/actions'; +/** + * Test a Resolver instance using jest, enzyme, and a mock data layer. + */ export class Simulator { + /** + * The redux store, creating in the constructor using the `dataAccessLayer`. + * This code subscribes to state transitions. + */ private readonly store: Store; + /** + * A fake 'History' API used with `react-router` to simulate a browser history. + */ private readonly history: HistoryPackageHistoryInterface; + /** + * The 'wrapper' returned by `enzyme` that contains the rendered Resolver react code. + */ private readonly wrapper: ReactWrapper; + /** + * A `redux` middleware that exposes all actions dispatched (along with the state at that point.) + * This is used by `debugActions`. + */ private readonly spyMiddleware: SpyMiddleware; constructor( dataAccessLayer: DataAccessLayer, + /** + * A string that uniquely identifies this Resolver instance amoung others mounted in the DOM. + */ private readonly resolverComponentInstanceID: string ) { + // create the spy middleware (for debugging tests) this.spyMiddleware = spyMiddlewareFactory(); + /** + * Create the real resolver middleware with a fake data access layer. + * By providing different data access layers, you can simulate different data and server environments. + */ const middlewareEnhancer = applyMiddleware( resolverMiddlewareFactory(dataAccessLayer), + // install the spyMiddleware this.spyMiddleware.middleware ); + // Create a redux store w/ the top level Resolver reducer and the enhancer that includes the Resolver middleware and the spyMiddleware this.store = createStore(resolverReducer, middlewareEnhancer); + // Create a fake 'history' instance that Resolver will use to read and write query string values this.history = createMemoryHistory(); // Render Resolver via the `MockResolver` component, using `enzyme`. @@ -49,22 +77,41 @@ export class Simulator { connectEnzymeWrapperAndStore(this.store, this.wrapper); } - public debugActions(): () => void { + /** + * Call this to console.log actions (and state). Use this to debug your tests. + * State and actions aren't exposed otherwise because the tests using this simulator should + * assert stuff about the DOM instead of internal state. Use selector/middleware/reducer + * unit tests to test that stuff. + */ + public debugActions(): /** + * Optionally call this to stop debugging actions. + */ () => void { return this.spyMiddleware.debugActions(); } /** * Return a promise that resolves after the `store`'s next state transition. + * Used by `mapStateTransisions` */ - public stateTransitioned(): Promise { + private stateTransitioned(): Promise { + // keep track of the resolve function of the promise that has been returned. let resolveState: (() => void) | null = null; + const promise: Promise = new Promise((resolve) => { + // immedatiely expose the resolve function in the outer scope. it will be resolved when the next state transition occurs. resolveState = resolve; }); + + // Subscribe to the store const unsubscribe = this.store.subscribe(() => { + // Once a state transition occurs, unsubscribe. unsubscribe(); + // Resolve the promise. The null assertion is safe here as Promise initializers run immediately (according to spec and node/browser implementations.) + // NB: the state is not resolved here. Code using the simulator should not rely on state or selectors of state. resolveState!(); }); + + // Return the promise that will be resolved on the next state transition, allowing code to `await` for the next state transition. return promise; } @@ -72,29 +119,51 @@ export class Simulator { * This will yield the return value of `mapper` after each state transition. If no state transition occurs for 10 event loops in a row, this will give up. */ public async *mapStateTransisions(mapper: () => R): AsyncIterable { + // Yield the value before any state transitions have occurred. yield mapper(); + + /** Increment this each time an event loop completes without a state transition. + * If this value hits `10`, end the loop. + * + * Code will test assertions after each state transition. If the assertion hasn't passed and no further state transitions occur, + * then the jest timeout will happen. The timeout doesn't give a useful message about the assertion. + * By shortcircuiting this function, code that uses it can short circuit the test timeout and print a useful error message. + * + * NB: the logic to shortcircuit the loop is here because knowledge of state is a concern of the simulator, not tests. + */ let timeoutCount = 0; while (true) { - const maybeValue: { value: R; timedOut: false } | { timedOut: true } = await Promise.race([ + /** + * `await` a race between the next state transition and a timeout that happens after `0`ms. + * If the timeout wins, no `dispatch` call caused a state transition in the last loop. + * If this keeps happening, assume that Resolver isn't going to do anythig else. + * + * If Resolver adds intentional delay logic (e.g. waiting before making a request), this code might have to change. + * In that case, Resolver should use the side effect context to schedule future work. This code could then subscribe to some event published by the side effect context. That way, this code will be aware of Resolver's intention to do work. + */ + const timedOut: boolean = await Promise.race([ (async (): Promise<{ value: R; timedOut: false }> => { await this.stateTransitioned(); - return { - value: mapper(), - timedOut: false, - }; + // If a state transition occurs, return false for `timedOut` + return false; })(), new Promise<{ timedOut: true }>((resolve) => { setTimeout(() => { - return resolve({ timedOut: true }); + // If a timeout occurs, resolve `timedOut` as true + return resolve(true); }, 0); }), ]); - if (maybeValue.timedOut) { + + if (timedOut) { + // If a timout occurred, note it. timeoutCount++; if (timeoutCount === 10) { + // if 10 timeouts happen in a row, end the loop early return; } } else { + // If a state transition occurs, reset the timout count and yield the value timeoutCount = 0; yield mapper(); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index ca99d8f2e6999d..47eb5fb57094f1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -5,7 +5,7 @@ */ import { oneAncestorTwoChildren } from '../data_access_layer/mocks/one_ancestor_two_children'; -import { Simulator } from '../models/simulator'; +import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher import '../test_utilities/extend_jest'; From 56aa8f225db6140f04a2c4c78caf7e377484e382 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 12:55:10 -0400 Subject: [PATCH 04/29] comments --- .../resolver/test_utilities/simulator.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx index 63d35e98e6059e..50e35c6012ec4d 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx @@ -186,7 +186,7 @@ export class Simulator { } /** - * true if a process node element is found for the entityID and if it has an [aria-selected] attribute. + * true if a process node element is found for the entityID and if it *does not have* an [aria-selected] attribute. */ public processNodeElementLooksUnselected(entityID: string): boolean { // find the process node, then exclude it if its selected. @@ -198,10 +198,7 @@ export class Simulator { } /** - * Given a `History` and a `resolverDocumentID`, return any values stored in the query string. - * This isn't exactly the same as the query string state, because parsing that from the query string - * would be business logic. For example, this doesn't ignore duplicates. - * Use this for testing. + * Return the selected node query string values. */ public queryStringValues(): { selectedNode: string[] } { const urlSearchParams = new URLSearchParams(this.history.location.search); @@ -234,7 +231,7 @@ export class Simulator { /** * Like `this.wrapper.find` but only returns DOM nodes. */ - public findInDOM(selector: string): ReactWrapper { + private findInDOM(selector: string): ReactWrapper { return this.wrapper.find(selector).filterWhere((wrapper) => typeof wrapper.type() === 'string'); } } @@ -242,10 +239,19 @@ export class Simulator { const baseResolverSelector = '[data-test-subj="resolver:node"]'; interface ProcessNodeElementSelectorOptions { + /** + * Entity ID of the node. If passed, will be used to create an data-attribtue CSS selector that should only get the related node element. + */ entityID?: string; + /** + * If true, only get nodes with an `[aria-selected="true"]` attribute. + */ selected?: boolean; } +/** + * An `enzyme` supported CSS selector for process node elements. + */ function processNodeElementSelector({ entityID, selected = false, From 3691bf05ad785fd25d6c88ea286cdd79dc00a324 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 12:58:50 -0400 Subject: [PATCH 05/29] moving files --- .../mock_resolver.tsx} | 20 +++++++++++-------- .../resolver/test_utilities/simulator.tsx | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) rename x-pack/plugins/security_solution/public/resolver/{view/mock.tsx => test_utilities/mock_resolver.tsx} (90%) diff --git a/x-pack/plugins/security_solution/public/resolver/view/mock.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/mock_resolver.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/resolver/view/mock.tsx rename to x-pack/plugins/security_solution/public/resolver/test_utilities/mock_resolver.tsx index d3e65e3d86e38b..851ead9c9f6f7e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/mock.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/mock_resolver.tsx @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// TODO, this is really part of the simulator. move it - /* eslint-disable no-duplicate-imports */ - /* eslint-disable react/display-name */ import React, { useMemo, useEffect, useState, useCallback } from 'react'; @@ -21,15 +18,22 @@ import { coreMock } from '../../../../../../src/core/public/mocks'; import { CoreStart } from '../../../../../../src/core/public'; import { ResolverState, SideEffectSimulator, ResolverProps } from '../types'; import { ResolverAction } from '../store/actions'; -import { ResolverWithoutProviders } from './resolver_without_providers'; -import { SideEffectContext } from './side_effect_context'; -import { sideEffectSimulator } from './side_effect_simulator'; +import { ResolverWithoutProviders } from '../view/resolver_without_providers'; +import { SideEffectContext } from '../view/side_effect_context'; +import { sideEffectSimulator } from '../view/side_effect_simulator'; type MockResolverProps = { - // core start and history can be optionally passed + /** + * Used for the KibanaContextProvider. Defaulted if not provided. + */ coreStart?: CoreStart; + /** + * Used for `react-router`. Defaulted if not provided. + */ history?: React.ComponentProps['history']; - // If passed, set the raster width to this value. Defaults to 800 + /** + * Used to simulate a raster width. Defaults to 800. + */ rasterWidth?: number; // If passed, set the raster height to this value. Defaults to 800 rasterHeight?: number; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx index 50e35c6012ec4d..1ff638b3d7904b 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx @@ -12,7 +12,7 @@ import { connectEnzymeWrapperAndStore } from './connect_enzyme_wrapper_and_store import { spyMiddlewareFactory } from './spy_middleware'; import { resolverMiddlewareFactory } from '../store/middleware'; import { resolverReducer } from '../store/reducer'; -import { MockResolver } from '../view/mock'; +import { MockResolver } from './mock_resolver'; import { ResolverState, DataAccessLayer, SpyMiddleware } from '../types'; import { ResolverAction } from '../store/actions'; From c34148ba20d476f5e79111fca5bc13a63fbec934 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 13:48:11 -0400 Subject: [PATCH 06/29] moving files --- .../{simulator.tsx => simulator/index.tsx} | 12 ++++++------ .../{ => simulator}/mock_resolver.tsx | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) rename x-pack/plugins/security_solution/public/resolver/test_utilities/{simulator.tsx => simulator/index.tsx} (96%) rename x-pack/plugins/security_solution/public/resolver/test_utilities/{ => simulator}/mock_resolver.tsx (88%) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx rename to x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 1ff638b3d7904b..25b90b52289f97 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { Store, createStore, applyMiddleware } from 'redux'; import { mount, ReactWrapper } from 'enzyme'; import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; -import { connectEnzymeWrapperAndStore } from './connect_enzyme_wrapper_and_store'; -import { spyMiddlewareFactory } from './spy_middleware'; -import { resolverMiddlewareFactory } from '../store/middleware'; -import { resolverReducer } from '../store/reducer'; +import { connectEnzymeWrapperAndStore } from '../connect_enzyme_wrapper_and_store'; +import { spyMiddlewareFactory } from '../spy_middleware'; +import { resolverMiddlewareFactory } from '../../store/middleware'; +import { resolverReducer } from '../../store/reducer'; import { MockResolver } from './mock_resolver'; -import { ResolverState, DataAccessLayer, SpyMiddleware } from '../types'; -import { ResolverAction } from '../store/actions'; +import { ResolverState, DataAccessLayer, SpyMiddleware } from '../../types'; +import { ResolverAction } from '../../store/actions'; /** * Test a Resolver instance using jest, enzyme, and a mock data layer. diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/resolver/test_utilities/mock_resolver.tsx rename to x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 851ead9c9f6f7e..6e5218d82020b4 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -13,14 +13,14 @@ import { createMemoryHistory } from 'history'; import { I18nProvider } from '@kbn/i18n/react'; import { Provider } from 'react-redux'; import { Store } from 'redux'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { coreMock } from '../../../../../../src/core/public/mocks'; -import { CoreStart } from '../../../../../../src/core/public'; -import { ResolverState, SideEffectSimulator, ResolverProps } from '../types'; -import { ResolverAction } from '../store/actions'; -import { ResolverWithoutProviders } from '../view/resolver_without_providers'; -import { SideEffectContext } from '../view/side_effect_context'; -import { sideEffectSimulator } from '../view/side_effect_simulator'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { ResolverState, SideEffectSimulator, ResolverProps } from '../../types'; +import { ResolverAction } from '../../store/actions'; +import { ResolverWithoutProviders } from '../../view/resolver_without_providers'; +import { SideEffectContext } from '../../view/side_effect_context'; +import { sideEffectSimulator } from '../../view/side_effect_simulator'; type MockResolverProps = { /** From 562e67254d4813a414e77798fb295e54adbefc24 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 13:59:07 -0400 Subject: [PATCH 07/29] comments and refactorin --- .../mocks/one_ancestor_two_children.ts | 23 ++++++++++- .../test_utilities/simulator/index.tsx | 14 +++++++ .../simulator/mock_resolver.tsx | 40 +++++++++---------- .../resolver/view/clickthrough.test.tsx | 6 ++- 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts index 025cc04d5f4686..b0065c023d31e7 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts @@ -14,7 +14,27 @@ import { mockTreeWithNoAncestorsAnd2Children } from '../../store/mocks/resolver_ import { DataAccessLayer } from '../../types'; interface Metadata { - entityIDs: { origin: 'origin'; firstChild: 'firstChild'; secondChild: 'secondChild' }; + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: { + /** + * The entityID of the node related to the document being analyzed. + */ + origin: 'origin'; + /** + * The entityID of the first child of the origin. + */ + firstChild: 'firstChild'; + /** + * The entityID of the second child of the origin. + */ + secondChild: 'secondChild'; + }; } /** @@ -22,6 +42,7 @@ interface Metadata { */ export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } { const metadata: Metadata = { + databaseDocumentID: '_id', entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, }; return { diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 25b90b52289f97..326a81d7b45daf 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { Store, createStore, applyMiddleware } from 'redux'; import { mount, ReactWrapper } from 'enzyme'; import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { connectEnzymeWrapperAndStore } from '../connect_enzyme_wrapper_and_store'; import { spyMiddlewareFactory } from '../spy_middleware'; import { resolverMiddlewareFactory } from '../../store/middleware'; @@ -39,6 +41,13 @@ export class Simulator { */ private readonly spyMiddleware: SpyMiddleware; constructor( + /** + * a databaseDocumentID to pass to Resolver. Resolver will use this in requests to the mock data layer. + */ + databaseDocumentID?: string, + /** + * A (mock) data access layer that will be used to create the Resolver store. + */ dataAccessLayer: DataAccessLayer, /** * A string that uniquely identifies this Resolver instance amoung others mounted in the DOM. @@ -64,12 +73,17 @@ export class Simulator { // Create a fake 'history' instance that Resolver will use to read and write query string values this.history = createMemoryHistory(); + // Used for KibanaContextProvider + const coreStart: CoreStart = coreMock.createStart(); + // Render Resolver via the `MockResolver` component, using `enzyme`. this.wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 6e5218d82020b4..8a69e0f2e1a1b5 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -24,23 +24,27 @@ import { sideEffectSimulator } from '../../view/side_effect_simulator'; type MockResolverProps = { /** - * Used for the KibanaContextProvider. Defaulted if not provided. + * Used to simulate a raster width. Defaults to 800. */ - coreStart?: CoreStart; + rasterWidth?: number; /** - * Used for `react-router`. Defaulted if not provided. + * Used to simulate a raster height. Defaults to 800. */ - history?: React.ComponentProps['history']; + rasterHeight?: number; /** - * Used to simulate a raster width. Defaults to 800. + * Used for the KibanaContextProvider */ - rasterWidth?: number; - // If passed, set the raster height to this value. Defaults to 800 - rasterHeight?: number; + coreStart: CoreStart; + /** + * Used for `react-router`. + */ + history: React.ComponentProps['history']; /** Pass a resolver store. See `storeFactory` and `mockDataAccessLayer` */ store: Store; - // All the props from `ResolverWithoutStore` can be optionally passed. -} & Partial; + /** + * All the props from `ResolverWithoutStore` can be passed. These aren't defaulted to anything (you might want to test what happens when they aren't present.) + */ +} & ResolverProps; /** * This is a mock Resolver component. It has faked versions of various services: @@ -58,14 +62,6 @@ type MockResolverProps = { * Use this in jest tests. Render it w/ `@testing-library/react` or `enzyme`. Then either interact with the result using fake events, or dispatch actions to the store. You could also pass in a store with initial data. */ export const MockResolver = React.memo((props: MockResolverProps) => { - // Get the coreStart services from props, or create them if needed. - const coreStart: CoreStart = useMemo(() => props.coreStart ?? coreMock.createStart(), [ - props.coreStart, - ]); - - // Get the history object from props, or create it if needed. - const history = useMemo(() => props.history ?? createMemoryHistory(), [props.history]); - const [resolverElement, setResolverElement] = useState(null); // Get a ref to the underlying Resolver element so we can resize. @@ -98,14 +94,14 @@ export const MockResolver = React.memo((props: MockResolverProps) => { return ( - - + + diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 47eb5fb57094f1..1f4e2f277a947f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -11,6 +11,7 @@ import '../test_utilities/extend_jest'; describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', () => { let simulator: Simulator; + let databaseDocumentID: string; let entityIDs: { origin: string; firstChild: string; secondChild: string }; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances @@ -23,8 +24,11 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( // save a reference to the entity IDs exposed by the mock data layer entityIDs = dataAccessLayerMetadata.entityIDs; + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + // create a resolver simulator, using the data access layer and an arbitrary component instance ID - simulator = new Simulator(dataAccessLayer, resolverComponentInstanceID); + simulator = new Simulator(databaseDocumentID, dataAccessLayer, resolverComponentInstanceID); }); describe('when it has loaded', () => { From 22cc9ee6a7cc93341c12d42fdc0198937332b7d2 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:15:58 -0400 Subject: [PATCH 08/29] fix custom matcher type --- .../public/resolver/test_utilities/extend_jest.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index 87bbb21e9a46fa..1f72525ac4e295 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -14,9 +14,10 @@ export {}; declare global { /* eslint-disable @typescript-eslint/no-namespace */ namespace jest { - interface Matchers { - // Type the custom matcher - toSometimesYieldEqualTo(b: T): Promise; + interface Matchers { + toSometimesYieldEqualTo( + expectedYield: T extends AsyncIterable ? E : never + ): Promise; } } } @@ -24,7 +25,7 @@ declare global { expect.extend({ /** * A custom matcher that takes an async generator and compares each value it yields to an expected value. - * If any yielded value deep equals the expected value, the matcher will pass. + * If any yielded value deep-equals the expected value, the matcher will pass. * If the generator ends with none of the yielded values matching, it will fail. */ async toSometimesYieldEqualTo( From 4f1d9904a78ede4a03aea485749b7fd02c8cf4f6 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:18:53 -0400 Subject: [PATCH 09/29] rename side effect simulator --- .../resolver/test_utilities/simulator/mock_resolver.tsx | 6 ++---- ...effect_simulator.ts => side_effect_simulator_factory.ts} | 2 +- .../public/resolver/view/use_camera.test.tsx | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) rename x-pack/plugins/security_solution/public/resolver/view/{side_effect_simulator.ts => side_effect_simulator_factory.ts} (98%) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 8a69e0f2e1a1b5..61f143db232c1b 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -9,18 +9,16 @@ import React, { useMemo, useEffect, useState, useCallback } from 'react'; import { Router } from 'react-router-dom'; -import { createMemoryHistory } from 'history'; import { I18nProvider } from '@kbn/i18n/react'; import { Provider } from 'react-redux'; import { Store } from 'redux'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; import { CoreStart } from '../../../../../../../src/core/public'; import { ResolverState, SideEffectSimulator, ResolverProps } from '../../types'; import { ResolverAction } from '../../store/actions'; import { ResolverWithoutProviders } from '../../view/resolver_without_providers'; import { SideEffectContext } from '../../view/side_effect_context'; -import { sideEffectSimulator } from '../../view/side_effect_simulator'; +import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory'; type MockResolverProps = { /** @@ -70,7 +68,7 @@ export const MockResolver = React.memo((props: MockResolverProps) => { setResolverElement(element); }, []); - const simulator: SideEffectSimulator = useMemo(() => sideEffectSimulator(), []); + const simulator: SideEffectSimulator = useMemo(() => sideEffectSimulatorFactory(), []); // Resize the Resolver element to match the passed in props. Resolver is size dependent. useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator.ts b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts similarity index 98% rename from x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator.ts rename to x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts index 5e9073ba2d3c9a..de50dc472b0677 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts @@ -11,7 +11,7 @@ import { SideEffectSimulator } from '../types'; * Create mock `SideEffectors` for `SideEffectContext.Provider`. The `control` * object is used to control the mocks. */ -export const sideEffectSimulator: () => SideEffectSimulator = () => { +export const sideEffectSimulatorFactory: () => SideEffectSimulator = () => { // The set of mock `ResizeObserver` instances that currently exist const resizeObserverInstances: Set = new Set(); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 8ffe45edcfd8ac..b32d63283b547d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -14,7 +14,7 @@ import { Matrix3, ResolverStore, SideEffectSimulator } from '../types'; import { ResolverEvent } from '../../../common/endpoint/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../models/vector2'; -import { sideEffectSimulator } from './side_effect_simulator'; +import { sideEffectSimulatorFactory } from './side_effect_simulator_factory'; import { mockProcessEvent } from '../models/process_event_test_helpers'; import { mock as mockResolverTree } from '../models/resolver_tree'; import { ResolverAction } from '../store/actions'; @@ -39,7 +39,7 @@ describe('useCamera on an unpainted element', () => { return
; }; - simulator = sideEffectSimulator(); + simulator = sideEffectSimulatorFactory(); reactRenderResult = render( From 544c0767cde31869e9e333d4fee63947182ae1e5 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:26:46 -0400 Subject: [PATCH 10/29] type issues --- .../test_utilities/simulator/index.tsx | 29 ++++++++++++------- .../resolver/view/clickthrough.test.tsx | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 326a81d7b45daf..13f85b36ee92ca 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -22,6 +22,10 @@ import { ResolverAction } from '../../store/actions'; * Test a Resolver instance using jest, enzyme, and a mock data layer. */ export class Simulator { + /** + * A string that uniquely identifies this Resolver instance amoung others mounted in the DOM. + */ + private readonly resolverComponentInstanceID: string; /** * The redux store, creating in the constructor using the `dataAccessLayer`. * This code subscribes to state transitions. @@ -40,20 +44,25 @@ export class Simulator { * This is used by `debugActions`. */ private readonly spyMiddleware: SpyMiddleware; - constructor( - /** - * a databaseDocumentID to pass to Resolver. Resolver will use this in requests to the mock data layer. - */ - databaseDocumentID?: string, + constructor({ + dataAccessLayer, + resolverComponentInstanceID, + databaseDocumentID, + }: { /** * A (mock) data access layer that will be used to create the Resolver store. */ - dataAccessLayer: DataAccessLayer, + dataAccessLayer: DataAccessLayer; /** * A string that uniquely identifies this Resolver instance amoung others mounted in the DOM. */ - private readonly resolverComponentInstanceID: string - ) { + resolverComponentInstanceID: string; + /** + * a databaseDocumentID to pass to Resolver. Resolver will use this in requests to the mock data layer. + */ + databaseDocumentID?: string; + }) { + this.resolverComponentInstanceID = resolverComponentInstanceID; // create the spy middleware (for debugging tests) this.spyMiddleware = spyMiddlewareFactory(); @@ -156,12 +165,12 @@ export class Simulator { * In that case, Resolver should use the side effect context to schedule future work. This code could then subscribe to some event published by the side effect context. That way, this code will be aware of Resolver's intention to do work. */ const timedOut: boolean = await Promise.race([ - (async (): Promise<{ value: R; timedOut: false }> => { + (async (): Promise => { await this.stateTransitioned(); // If a state transition occurs, return false for `timedOut` return false; })(), - new Promise<{ timedOut: true }>((resolve) => { + new Promise((resolve) => { setTimeout(() => { // If a timeout occurs, resolve `timedOut` as true return resolve(true); diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 1f4e2f277a947f..fb334b45259a67 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -28,7 +28,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; // create a resolver simulator, using the data access layer and an arbitrary component instance ID - simulator = new Simulator(databaseDocumentID, dataAccessLayer, resolverComponentInstanceID); + simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); }); describe('when it has loaded', () => { From 43495522a4bd3e061ef159eef0ea876e30869041 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:34:01 -0400 Subject: [PATCH 11/29] comments --- .../public/resolver/data_access_layer/factory.ts | 14 +++++++++++++- .../mocks/one_ancestor_two_children.ts | 2 +- .../connect_enzyme_wrapper_and_store.ts | 6 +++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 4a1c57566de725..0baab659c912d0 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -15,27 +15,39 @@ import { import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants'; /** - * The only concrete DataAccessLayer. This isn't built in to Resolver. Instead we inject it. This way, tests can provide a fake one. + * The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead. */ export function dataAccessLayerFactory( context: KibanaReactContextValue ): DataAccessLayer { const dataAccessLayer: DataAccessLayer = { + /** + * Used to get non-process related events for a node. + */ async relatedEvents(entityID: string): Promise { return context.services.http.get(`/api/endpoint/resolver/${entityID}/events`, { query: { events: 100 }, }); }, + /** + * Used to get descendant and ancestor process events for a node. + */ async resolverTree(entityID: string, signal: AbortSignal): Promise { return context.services.http.get(`/api/endpoint/resolver/${entityID}`, { signal, }); }, + /** + * Used to get the default index pattern from the SIEM app. + */ indexPatterns(): string[] { return context.services.uiSettings.get(defaultIndexKey); }, + /** + * Used to get the entity_id for an _id. + */ async entities({ _id, indices, diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts index b0065c023d31e7..be0bc1b812a0b4 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts @@ -38,7 +38,7 @@ interface Metadata { } /** - * Simplest mock dataAccessLayer possible. + * A simple mock dataAccessLayer possible that returns a tree with 0 ancestors and 2 direct children. 1 related event is returned. The parameter to `entities` is ignored. */ export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } { const metadata: Metadata = { diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts index 9be69bd9d4a705..f38eec991b1e6f 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts @@ -7,9 +7,13 @@ import { Store } from 'redux'; import { ReactWrapper } from 'enzyme'; +/** + * We use the full-DOM emulation mode of `enzyme` via `mount`. Even though we use `react-redux`, `enzyme` + * does not update the DOM after state transitions. This subscribes to the `redux` store and after any state + * transition it asks `enzyme` to update the DOM to match the React state. + */ export function connectEnzymeWrapperAndStore(store: Store, wrapper: ReactWrapper): void { store.subscribe(() => { - // update the enzyme wrapper after each state transition return wrapper.update(); }); } From ec6f28a20d9e5852c8dbe16a555d596c0dca7074 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:40:42 -0400 Subject: [PATCH 12/29] fix comments --- .../simulator/mock_resolver.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 61f143db232c1b..b112e9ebbdce9d 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -22,11 +22,11 @@ import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_fac type MockResolverProps = { /** - * Used to simulate a raster width. Defaults to 800. + * Used to simulate a raster width. Defaults to 1600. */ rasterWidth?: number; /** - * Used to simulate a raster height. Defaults to 800. + * Used to simulate a raster height. Defaults to 1200. */ rasterHeight?: number; /** @@ -45,19 +45,18 @@ type MockResolverProps = { } & ResolverProps; /** - * This is a mock Resolver component. It has faked versions of various services: - * * fake i18n - * * fake (memory) history (optionally provided) - * * fake coreStart services (optionally provided) - * * SideEffectContext + * This is a mock Resolver component. It is intended to be used with `enzyme` tests via the `Simulator` class. It wraps Resolver in the required providers: + * * `i18n` + * * `Router` using a provided `History` + * * `SideEffectContext.Provider` using side effect simulator it creates + * * `KibanaContextProvider` using a provided `CoreStart` instance + * * `react-redux`'s `Provider` using a provided `Store`. * - * You will need to provide a store. Create one with `storyFactory`. The store will need a mock `DataAccessLayer`. - * - * Props required by `ResolverWithoutStore` can be passed as well. If not passed, they are defaulted. - * * `databaseDocumentID` - * * `resolverComponentInstanceID` - * - * Use this in jest tests. Render it w/ `@testing-library/react` or `enzyme`. Then either interact with the result using fake events, or dispatch actions to the store. You could also pass in a store with initial data. + * Resolver needs to measure its size in the DOM. The `SideEffectSimulator` instance can fake the size of an element. + * However in tests, React doesn't have good DOM reconciliation and the root element is often swapped out. When the + * element is replaced, the fake dimensions stop being applied. In order to get around this issue, this component will + * trigger a simulated resize on the root node reference any time it changes. This simulates the layout process a real + * browser would do when an element is attached to the DOM. */ export const MockResolver = React.memo((props: MockResolverProps) => { const [resolverElement, setResolverElement] = useState(null); From 6a86e70ea16784c7c9bf870b112df0e928326f19 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:57:00 -0400 Subject: [PATCH 13/29] use weak map for content rects in side effect simulator --- .../public/resolver/view/side_effect_simulator_factory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts index de50dc472b0677..25be222e2fe4a7 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/side_effect_simulator_factory.ts @@ -16,7 +16,8 @@ export const sideEffectSimulatorFactory: () => SideEffectSimulator = () => { const resizeObserverInstances: Set = new Set(); // A map of `Element`s to their fake `DOMRect`s - const contentRects: Map = new Map(); + // Use a `WeakMap` since elements can be removed from the DOM. + const contentRects: WeakMap = new Map(); /** * Simulate an element's size changing. This will trigger any `ResizeObserverCallback`s which From 9458e3c7b3faa5e4869a09ca86d264c5dc671f4b Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:57:37 -0400 Subject: [PATCH 14/29] dunno what i did there --- .../security_solution/public/resolver/view/edge_line.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 65c70f94432c79..9f310bb1cc0d65 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -45,7 +45,7 @@ const StyledElapsedTime = styled.div` left: ${(props) => `${props.leftPct}%`}; padding: 6px 8px; border-radius: 999px; // generate pill shape - transform: translate(-50%, -50%) rotateX(35deg); + transform: translate(-50%, -50%); user-select: none; `; From 4bd17ed18b63367e403c602b4e4fa119e06a1b08 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 14:59:27 -0400 Subject: [PATCH 15/29] cleanup --- .../public/resolver/view/panels/panel_content_utilities.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 9b68df46f69b6b..55b5be21fb4a45 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -27,13 +27,7 @@ const BetaHeader = styled(`header`)` * The two query parameters we read/write on to control which view the table presents: */ export interface CrumbInfo { - /** - * @deprecated - */ crumbId: string; - /** - * @deprecated - */ crumbEvent: string; } From a72c164e8a747271377288ae449051ef5cb6a4c0 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 15:14:28 -0400 Subject: [PATCH 16/29] comments. file name --- .../public/resolver/test_utilities/simulator/index.tsx | 2 +- .../{spy_middleware.ts => spy_middleware_factory.ts} | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) rename x-pack/plugins/security_solution/public/resolver/test_utilities/{spy_middleware.ts => spy_middleware_factory.ts} (84%) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 13f85b36ee92ca..383d3dc432560e 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -11,7 +11,7 @@ import { createMemoryHistory, History as HistoryPackageHistoryInterface } from ' import { CoreStart } from '../../../../../../../src/core/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { connectEnzymeWrapperAndStore } from '../connect_enzyme_wrapper_and_store'; -import { spyMiddlewareFactory } from '../spy_middleware'; +import { spyMiddlewareFactory } from '../spy_middleware_factory'; import { resolverMiddlewareFactory } from '../../store/middleware'; import { resolverReducer } from '../../store/reducer'; import { MockResolver } from './mock_resolver'; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts similarity index 84% rename from x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware.ts rename to x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts index 7878e6849034e5..aea914e4c32d92 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts @@ -7,7 +7,10 @@ import { ResolverAction } from '../store/actions'; import { SpyMiddleware, SpyMiddlewareStateActionPair } from '../types'; -// TODO, rename file +/** + * Return a `SpyMiddleware` to be used in testing. Use `debugActions` to console.log actions and the state they produced. + * For reducer/middleware tests, you can use `actions` to get access to each dispatched action along w/ the state it produced. + */ export const spyMiddlewareFactory: () => SpyMiddleware = () => { const resolvers: Set<(stateActionPair: SpyMiddlewareStateActionPair) => void> = new Set(); From 0a7d1bdf0a2740c4e7f8dc9641c31bd25a49b6e7 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 15:16:29 -0400 Subject: [PATCH 17/29] comments --- .../security_solution/public/resolver/types.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 80d99109eae39c..0a1e7b4202bfba 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -500,11 +500,22 @@ export interface ResolverProps { resolverComponentInstanceID: string; } +/** + * Used by `SpyMiddleware`. + */ export interface SpyMiddlewareStateActionPair { + /** An action dispatched, `state` is the state that the reducer returned when handling this action. + */ action: ResolverAction; + /** + * A resolver state that was returned by the reducer when handling `action`. + */ state: ResolverState; } +/** + * A wrapper object that has a middleware along with an async generator that returns the actions dispatched to the store (along with state.) + */ export interface SpyMiddleware { /** * A middleware to use with `applyMiddleware`. From 7916e1d41771824787cd51ceea563515e619c3e7 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 15:19:29 -0400 Subject: [PATCH 18/29] comments --- .../public/resolver/view/resolver_without_providers.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 2584cc34a1062d..3b28a9ba490435 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -30,6 +30,9 @@ import { ResolverProps } from '../types'; * The highest level connected Resolver component. Needs a `Provider` in its ancestry to work. */ export const ResolverWithoutProviders = React.memo( + /** + * Use `forwardRef` so that the `Simulator` used in testing can access the top level DOM element. + */ React.forwardRef(function ( { className, databaseDocumentID, resolverComponentInstanceID }: ResolverProps, refToForward From c99fd73872a9638182382072275e0314389f7a90 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 15:19:57 -0400 Subject: [PATCH 19/29] oops --- .../plugins/security_solution/public/resolver/view/submenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 2499a451b9c8c2..6a9ab184e9bab0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -190,7 +190,7 @@ const NodeSubMenuComponents = React.memo( * then force the popover to reposition itself. */ popoverRef.current && - !projectionMatrixAtLastRender.current && + projectionMatrixAtLastRender.current && projectionMatrixAtLastRender.current !== projectionMatrix ) { popoverRef.current.positionPopoverFixed(); From 38cd6604646eadecb236ea02f56f839ed738ac7d Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 15:43:41 -0400 Subject: [PATCH 20/29] comment fixed --- x-pack/plugins/security_solution/public/resolver/view/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index cf96fae40aa31e..d9a0bf291d0e43 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -15,7 +15,7 @@ import { dataAccessLayerFactory } from '../data_access_layer/factory'; import { ResolverWithoutProviders } from './resolver_without_providers'; /** - * The `Resolver` component to use. This sets up the DataAccessLayer provider. Use `ResolverWithoutStore` in tests or in other scenarios where you want to provide a different (or fake) data access layer. + * The `Resolver` component to use. This sets up the DataAccessLayer provider. Use `ResolverWithoutProviders` in tests or in other scenarios where you want to provide a different (or fake) data access layer. */ export const Resolver = React.memo((props: ResolverProps) => { const context = useKibana(); From a6f18fa8cd26cd320e47b6e22244b0e9992653ff Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 15:47:10 -0400 Subject: [PATCH 21/29] more comments --- .../resolver/test_utilities/connect_enzyme_wrapper_and_store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts index f38eec991b1e6f..3a4a1f7d634d11 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/connect_enzyme_wrapper_and_store.ts @@ -14,6 +14,7 @@ import { ReactWrapper } from 'enzyme'; */ export function connectEnzymeWrapperAndStore(store: Store, wrapper: ReactWrapper): void { store.subscribe(() => { + // See https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/update.html return wrapper.update(); }); } From 73047b6072f5bb2b513815ef490ab30813b50618 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 16:13:11 -0400 Subject: [PATCH 22/29] spelling --- .../resolver/data_access_layer/factory.ts | 2 +- .../public/resolver/models/process_event.ts | 14 +++---- .../public/resolver/store/middleware/index.ts | 4 +- .../resolver/test_utilities/extend_jest.ts | 2 +- .../test_utilities/simulator/index.tsx | 20 ++++----- .../simulator/mock_resolver.tsx | 2 +- .../test_utilities/spy_middleware_factory.ts | 2 +- .../public/resolver/types.ts | 42 +++++++++---------- .../panels/panel_content_process_detail.tsx | 2 +- .../view/use_resolver_query_params.ts | 4 +- 10 files changed, 47 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 0baab659c912d0..016ebfa0faee40 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -39,7 +39,7 @@ export function dataAccessLayerFactory( }, /** - * Used to get the default index pattern from the SIEM app. + * Used to get the default index pattern from the SIEM application. */ indexPatterns(): string[] { return context.services.uiSettings.get(defaultIndexKey); diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index af465c77c4ba8c..1a5c67f6a6f2ff 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -29,7 +29,7 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) { } /** - * ms since unix epoc, based on timestamp. + * ms since Unix epoc, based on timestamp. * may return NaN if the timestamp wasn't present or was invalid. */ export function datetime(passedEvent: ResolverEvent): number | null { @@ -85,7 +85,7 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType { } /** - * Returns the process event's pid + * Returns the process event's PID */ export function uniquePidForProcess(passedEvent: ResolverEvent): string { if (event.isLegacyEvent(passedEvent)) { @@ -96,7 +96,7 @@ export function uniquePidForProcess(passedEvent: ResolverEvent): string { } /** - * Returns the pid for the process on the host + * Returns the PID for the process on the host */ export function processPid(passedEvent: ResolverEvent): number | undefined { if (event.isLegacyEvent(passedEvent)) { @@ -107,7 +107,7 @@ export function processPid(passedEvent: ResolverEvent): number | undefined { } /** - * Returns the process event's parent pid + * Returns the process event's parent PID */ export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined { if (event.isLegacyEvent(passedEvent)) { @@ -118,7 +118,7 @@ export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | } /** - * Returns the process event's parent pid + * Returns the process event's parent PID */ export function processParentPid(passedEvent: ResolverEvent): number | undefined { if (event.isLegacyEvent(passedEvent)) { @@ -149,7 +149,7 @@ export function userInfoForProcess( } /** - * Returns the MD5 hash for the `passedEvent` param, or undefined if it can't be located + * Returns the MD5 hash for the `passedEvent` parameter, or undefined if it can't be located * @param {ResolverEvent} passedEvent The `ResolverEvent` to get the MD5 value for * @returns {string | undefined} The MD5 string for the event */ @@ -164,7 +164,7 @@ export function md5HashForProcess(passedEvent: ResolverEvent): string | undefine /** * Returns the command line path and arguments used to run the `passedEvent` if any * - * @param {ResolverEvent} passedEvent The `ResolverEvent` to get the arguemnts value for + * @param {ResolverEvent} passedEvent The `ResolverEvent` to get the arguments value for * @returns {string | undefined} The arguments (including the path) used to run the process */ export function argsForProcess(passedEvent: ResolverEvent): string | undefined { diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index 4ddfac12c11940..ef6b1f5eb3c6f7 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -17,9 +17,9 @@ type MiddlewareFactory = ( ) => (next: Dispatch) => (action: ResolverAction) => unknown; /** - * The redux middleware that the app uses to trigger side effects. + * The `redux` middleware that the application uses to trigger side effects. * All data fetching should be done here. - * For actions that the app triggers directly, use `app` as a prefix for the type. + * For actions that the application triggers directly, use `app` as a prefix for the type. * For actions that are triggered as a result of server interaction, use `server` as a prefix for the type. */ export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: DataAccessLayer) => { diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index 1f72525ac4e295..f244a6481fdd3c 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -46,7 +46,7 @@ expect.extend({ // Set to true if the test passes. let pass: boolean = false; - // Aysync iterate over the iterable + // Async iterate over the iterable for await (const received of receivedIterable) { // keep track of the last value. Used in both pass and fail messages lastReceived = received; diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 383d3dc432560e..94b92e62e1f4a4 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -23,7 +23,7 @@ import { ResolverAction } from '../../store/actions'; */ export class Simulator { /** - * A string that uniquely identifies this Resolver instance amoung others mounted in the DOM. + * A string that uniquely identifies this Resolver instance among others mounted in the DOM. */ private readonly resolverComponentInstanceID: string; /** @@ -54,7 +54,7 @@ export class Simulator { */ dataAccessLayer: DataAccessLayer; /** - * A string that uniquely identifies this Resolver instance amoung others mounted in the DOM. + * A string that uniquely identifies this Resolver instance among others mounted in the DOM. */ resolverComponentInstanceID: string; /** @@ -76,13 +76,13 @@ export class Simulator { this.spyMiddleware.middleware ); - // Create a redux store w/ the top level Resolver reducer and the enhancer that includes the Resolver middleware and the spyMiddleware + // Create a redux store w/ the top level Resolver reducer and the enhancer that includes the Resolver middleware and the `spyMiddleware` this.store = createStore(resolverReducer, middlewareEnhancer); // Create a fake 'history' instance that Resolver will use to read and write query string values this.history = createMemoryHistory(); - // Used for KibanaContextProvider + // Used for `KibanaContextProvider` const coreStart: CoreStart = coreMock.createStart(); // Render Resolver via the `MockResolver` component, using `enzyme`. @@ -121,7 +121,7 @@ export class Simulator { let resolveState: (() => void) | null = null; const promise: Promise = new Promise((resolve) => { - // immedatiely expose the resolve function in the outer scope. it will be resolved when the next state transition occurs. + // Immediately expose the resolve function in the outer scope. It will be resolved when the next state transition occurs. resolveState = resolve; }); @@ -150,16 +150,16 @@ export class Simulator { * * Code will test assertions after each state transition. If the assertion hasn't passed and no further state transitions occur, * then the jest timeout will happen. The timeout doesn't give a useful message about the assertion. - * By shortcircuiting this function, code that uses it can short circuit the test timeout and print a useful error message. + * By short-circuiting this function, code that uses it can short circuit the test timeout and print a useful error message. * - * NB: the logic to shortcircuit the loop is here because knowledge of state is a concern of the simulator, not tests. + * NB: the logic to short-circuit the loop is here because knowledge of state is a concern of the simulator, not tests. */ let timeoutCount = 0; while (true) { /** * `await` a race between the next state transition and a timeout that happens after `0`ms. * If the timeout wins, no `dispatch` call caused a state transition in the last loop. - * If this keeps happening, assume that Resolver isn't going to do anythig else. + * If this keeps happening, assume that Resolver isn't going to do anything else. * * If Resolver adds intentional delay logic (e.g. waiting before making a request), this code might have to change. * In that case, Resolver should use the side effect context to schedule future work. This code could then subscribe to some event published by the side effect context. That way, this code will be aware of Resolver's intention to do work. @@ -186,7 +186,7 @@ export class Simulator { return; } } else { - // If a state transition occurs, reset the timout count and yield the value + // If a state transition occurs, reset the timeout count and yield the value timeoutCount = 0; yield mapper(); } @@ -263,7 +263,7 @@ const baseResolverSelector = '[data-test-subj="resolver:node"]'; interface ProcessNodeElementSelectorOptions { /** - * Entity ID of the node. If passed, will be used to create an data-attribtue CSS selector that should only get the related node element. + * Entity ID of the node. If passed, will be used to create an data-attribute CSS selector that should only get the related node element. */ entityID?: string; /** diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index b112e9ebbdce9d..36bb2a5ffc9fe9 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -30,7 +30,7 @@ type MockResolverProps = { */ rasterHeight?: number; /** - * Used for the KibanaContextProvider + * Used for the `KibanaContextProvider` */ coreStart: CoreStart; /** diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts index aea914e4c32d92..4e70740df9025e 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts @@ -9,7 +9,7 @@ import { SpyMiddleware, SpyMiddlewareStateActionPair } from '../types'; /** * Return a `SpyMiddleware` to be used in testing. Use `debugActions` to console.log actions and the state they produced. - * For reducer/middleware tests, you can use `actions` to get access to each dispatched action along w/ the state it produced. + * For reducer/middleware tests, you can use `actions` to get access to each dispatched action along with the state it produced. */ export const spyMiddlewareFactory: () => SpyMiddleware = () => { const resolvers: Set<(stateActionPair: SpyMiddlewareStateActionPair) => void> = new Set(); diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 0a1e7b4202bfba..38e0cd04835592 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -38,21 +38,21 @@ export interface ResolverState { } /** - * Piece of redux state that models an animation for the camera. + * Piece of `redux` state that models an animation for the camera. */ export interface ResolverUIState { /** - * The nodeID for the process that is selected (in the aria-activedescendent sense of being selected.) + * The `nodeID` for the process that is selected (in the `aria-activedescendent` sense of being selected.) */ readonly ariaActiveDescendant: string | null; /** - * nodeID of the selected node + * `nodeID` of the selected node */ readonly selectedNode: string | null; } /** - * Piece of redux state that models an animation for the camera. + * Piece of `redux` state that models an animation for the camera. */ export interface CameraAnimationState { /** @@ -76,7 +76,7 @@ export interface CameraAnimationState { } /** - * The redux state for the `useCamera` hook. + * The `redux` state for the `useCamera` hook. */ export type CameraState = { /** @@ -96,7 +96,7 @@ export type CameraState = { readonly translationNotCountingCurrentPanning: Vector2; /** - * The world coordinates that the pointing device was last over. This is used during mousewheel zoom. + * The world coordinates that the pointing device was last over. This is used during mouse-wheel zoom. */ readonly latestFocusedWorldCoordinates: Vector2 | null; } & ( @@ -143,7 +143,7 @@ export type CameraState = { export type IndexedEntity = IndexedEdgeLineSegment | IndexedProcessNode; /** - * The entity stored in rbush for resolver edge lines. + * The entity stored in `rbush` for resolver edge lines. */ export interface IndexedEdgeLineSegment extends BBox { type: 'edgeLine'; @@ -151,7 +151,7 @@ export interface IndexedEdgeLineSegment extends BBox { } /** - * The entity store in rbush for resolver process nodes. + * The entity store in `rbush` for resolver process nodes. */ export interface IndexedProcessNode extends BBox { type: 'processNode'; @@ -168,7 +168,7 @@ export interface VisibleEntites { } /** - * State for `data` reducer which handles receiving Resolver data from the backend. + * State for `data` reducer which handles receiving Resolver data from the back-end. */ export interface DataState { readonly relatedEvents: Map; @@ -221,11 +221,11 @@ export type Vector2 = readonly [number, number]; */ export interface AABB { /** - * Vector who's `x` component is the _left_ side of the AABB and who's `y` component is the _bottom_ side of the AABB. + * Vector who's `x` component is the _left_ side of the `AABB` and who's `y` component is the _bottom_ side of the `AABB`. **/ readonly minimum: Vector2; /** - * Vector who's `x` component is the _right_ side of the AABB and who's `y` component is the _bottom_ side of the AABB. + * Vector who's `x` component is the _right_ side of the `AABB` and who's `y` component is the _bottom_ side of the `AABB`. **/ readonly maximum: Vector2; } @@ -274,7 +274,7 @@ export interface ProcessEvent { } /** - * A represention of a process tree with indices for O(1) access to children and values by id. + * A representation of a process tree with indices for O(1) access to children and values by id. */ export interface IndexedProcessTree { /** @@ -288,7 +288,7 @@ export interface IndexedProcessTree { } /** - * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` + * A map of `ProcessEvents` (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` */ export type ProcessWidths = Map; /** @@ -326,16 +326,16 @@ export interface DurationDetails { */ export interface EdgeLineMetadata { elapsedTime?: DurationDetails; - // A string of the two joined process nodes concatted together. + // A string of the two joined process nodes concatenated together. uniqueId: string; } /** - * A tuple of 2 vector2 points forming a polyline. Used to connect process nodes in the graph. + * A tuple of 2 vector2 points forming a poly-line. Used to connect process nodes in the graph. */ export type EdgeLinePoints = Vector2[]; /** - * Edge line components including the points joining the edgeline and any optional associated metadata + * Edge line components including the points joining the edge-line and any optional associated metadata */ export interface EdgeLineSegment { points: EdgeLinePoints; @@ -343,7 +343,7 @@ export interface EdgeLineSegment { } /** - * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. + * Used to provide pre-calculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { process: ResolverEvent; @@ -431,11 +431,11 @@ export type ResolverStore = Store; */ export interface IsometricTaxiLayout { /** - * A map of events to position. each event represents its own node. + * A map of events to position. Each event represents its own node. */ processNodePositions: Map; /** - * A map of edgline segments, which graphically connect nodes. + * A map of edge-line segments, which graphically connect nodes. */ edgeLineSegments: EdgeLineSegment[]; @@ -494,8 +494,8 @@ export interface ResolverProps { */ databaseDocumentID?: string; /** - * A string literal describing where in the app resolver is located, - * used to prevent collisions in things like query params + * A string literal describing where in the application resolver is located. + * Used to prevent collisions in things like query parameters. */ resolverComponentInstanceID: string; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx index bdaba3f08575a8..7b5eb13359dbb3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx @@ -39,7 +39,7 @@ const StyledDescriptionList = styled(EuiDescriptionList)` /** * A description list view of all the Metadata that goes with a particular process event, like: - * Created, Pid, User/Domain, etc. + * Created, PID, User/Domain, etc. */ export const ProcessDetails = memo(function ProcessDetails({ processEvent, diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts index 5247e30c7749c3..ed514a61d4e068 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_resolver_query_params.ts @@ -24,8 +24,8 @@ export function useResolverQueryParams() { const uniqueCrumbEventKey: string = `resolver-${resolverComponentInstanceID}-event`; const pushToQueryParams = useCallback( (newCrumbs: CrumbInfo) => { - // Construct a new set of params from the current set (minus empty params) - // by assigning the new set of params provided in `newCrumbs` + // Construct a new set of parameters from the current set (minus empty parameters) + // by assigning the new set of parameters provided in `newCrumbs` const crumbsToPass = { ...querystring.parse(urlSearch.slice(1)), [uniqueCrumbIdKey]: newCrumbs.crumbId, From bcd96e290d6bae0f53fa23574ce57b4489b14c79 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Mon, 27 Jul 2020 16:15:45 -0400 Subject: [PATCH 23/29] spelling again --- .../public/resolver/test_utilities/simulator/index.tsx | 4 ++-- .../public/resolver/view/clickthrough.test.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 94b92e62e1f4a4..7a61427c56a3ba 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -114,7 +114,7 @@ export class Simulator { /** * Return a promise that resolves after the `store`'s next state transition. - * Used by `mapStateTransisions` + * Used by `mapStateTransitions` */ private stateTransitioned(): Promise { // keep track of the resolve function of the promise that has been returned. @@ -141,7 +141,7 @@ export class Simulator { /** * This will yield the return value of `mapper` after each state transition. If no state transition occurs for 10 event loops in a row, this will give up. */ - public async *mapStateTransisions(mapper: () => R): AsyncIterable { + public async *mapStateTransitions(mapper: () => R): AsyncIterable { // Yield the value before any state transitions have occurred. yield mapper(); diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index fb334b45259a67..321fb3a458561e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -40,7 +40,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( * * For example, there might be no loading element at one point, and 1 graph element at one point, but never a single time when there is both 1 graph element and 0 loading elements. */ - simulator.mapStateTransisions(() => ({ + simulator.mapStateTransitions(() => ({ graphElements: simulator.graphElement().length, graphLoadingElements: simulator.graphLoadingElement().length, graphErrorElements: simulator.graphErrorElement().length, @@ -73,7 +73,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( }); it('should render the second child node as selected, and the first child not as not selected, and the query string should indicate that the second child is selected', async () => { await expect( - simulator.mapStateTransisions(function value() { + simulator.mapStateTransitions(function value() { return { // the query string has a key showing that the second child is selected queryStringSelectedNode: simulator.queryStringValues().selectedNode, From 036891dc3436a68ca4d6ff22f4ef1766d67903b8 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 28 Jul 2020 10:19:27 -0400 Subject: [PATCH 24/29] WIP --- .../with_manually_controlled_responses.ts | 39 ++++++++++++++++++ .../public/resolver/view/specs/loading.tsx | 40 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts new file mode 100644 index 00000000000000..0fc43940ed7163 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts @@ -0,0 +1,39 @@ +/* + * 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 { DataAccessLayer } from '../../types'; + +export function withManuallyControlledResponses( + dataAccessLayer: DataAccessLayer +): { respond: () => void; dataAccessLayer: DataAccessLayer } { + const resolvers: Set<() => void> = new Set(); + return { + respond() { + const oldResolvers = [...resolvers]; + // clear the old set before resolving, since resolving could cause more things to be added to the set + resolvers.clear(); + for (const resolve of oldResolvers) { + resolve(); + } + }, + dataAccessLayer: { + ...dataAccessLayer, + relatedEvents: controlledPromise(dataAccessLayer.relatedEvents), + resolverTree: controlledPromise(dataAccessLayer.resolverTree), + entities: controlledPromise(dataAccessLayer.entities), + }, + }; + function controlledPromise( + fn: (this: DataAccessLayer, ...args: Args) => Promise + ): (this: DataAccessLayer, ...args: Args) => Promise { + return async function (...args: Args) { + await new Promise((resolve) => { + resolvers.add(resolve); + }); + return fn.call(this, ...args); + }; + } +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx b/x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx new file mode 100644 index 00000000000000..74fb352df85bc2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx @@ -0,0 +1,40 @@ +/* + * 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 '../../test_utilities/extend_jest'; + +/* eslint-disable prefer-const */ + +import { Simulator } from '../../test_utilities/simulator'; +import { oneAncestorTwoChildren } from '../../data_access_layer/mocks/one_ancestor_two_children'; +import { withManuallyControlledResponses } from '../../data_access_layer/mocks/with_manually_controlled_responses'; + +/** + * These specs define the loading behavior for the graph and panel. + */ +describe('when resolver is mounted with a databaseDocumentID', () => { + let respond: () => void; + let simulator: Simulator; + let resolverComponentInstanceID = 'resolverComponentInstanceID'; + let entityIDs: { origin: string; firstChild: string; secondChild: string }; + beforeEach(() => { + const { metadata, dataAccessLayer } = oneAncestorTwoChildren(); + const dataController = withManuallyControlledResponses(dataAccessLayer); + respond = dataController.respond; + simulator = new Simulator({ + databaseDocumentID: metadata.databaseDocumentID, + dataAccessLayer: dataController.dataAccessLayer, + resolverComponentInstanceID, + }); + }); + it('should show a loading message', async () => { + await expect( + simulator.mapStateTransitions(() => { + return simulator.graphLoadingElement().length; + }) + ).toSometimesYieldEqualTo(1); + }); +}); From acfa50aeeb2ec9d5cbde7e03494fda3a349b92fd Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 28 Jul 2020 10:19:34 -0400 Subject: [PATCH 25/29] Revert "WIP" This reverts commit 8f271d00919142035f868ab469a6d2a53b56b5b4. --- .../with_manually_controlled_responses.ts | 39 ------------------ .../public/resolver/view/specs/loading.tsx | 40 ------------------- 2 files changed, 79 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts delete mode 100644 x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts deleted file mode 100644 index 0fc43940ed7163..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/with_manually_controlled_responses.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { DataAccessLayer } from '../../types'; - -export function withManuallyControlledResponses( - dataAccessLayer: DataAccessLayer -): { respond: () => void; dataAccessLayer: DataAccessLayer } { - const resolvers: Set<() => void> = new Set(); - return { - respond() { - const oldResolvers = [...resolvers]; - // clear the old set before resolving, since resolving could cause more things to be added to the set - resolvers.clear(); - for (const resolve of oldResolvers) { - resolve(); - } - }, - dataAccessLayer: { - ...dataAccessLayer, - relatedEvents: controlledPromise(dataAccessLayer.relatedEvents), - resolverTree: controlledPromise(dataAccessLayer.resolverTree), - entities: controlledPromise(dataAccessLayer.entities), - }, - }; - function controlledPromise( - fn: (this: DataAccessLayer, ...args: Args) => Promise - ): (this: DataAccessLayer, ...args: Args) => Promise { - return async function (...args: Args) { - await new Promise((resolve) => { - resolvers.add(resolve); - }); - return fn.call(this, ...args); - }; - } -} diff --git a/x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx b/x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx deleted file mode 100644 index 74fb352df85bc2..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/view/specs/loading.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 '../../test_utilities/extend_jest'; - -/* eslint-disable prefer-const */ - -import { Simulator } from '../../test_utilities/simulator'; -import { oneAncestorTwoChildren } from '../../data_access_layer/mocks/one_ancestor_two_children'; -import { withManuallyControlledResponses } from '../../data_access_layer/mocks/with_manually_controlled_responses'; - -/** - * These specs define the loading behavior for the graph and panel. - */ -describe('when resolver is mounted with a databaseDocumentID', () => { - let respond: () => void; - let simulator: Simulator; - let resolverComponentInstanceID = 'resolverComponentInstanceID'; - let entityIDs: { origin: string; firstChild: string; secondChild: string }; - beforeEach(() => { - const { metadata, dataAccessLayer } = oneAncestorTwoChildren(); - const dataController = withManuallyControlledResponses(dataAccessLayer); - respond = dataController.respond; - simulator = new Simulator({ - databaseDocumentID: metadata.databaseDocumentID, - dataAccessLayer: dataController.dataAccessLayer, - resolverComponentInstanceID, - }); - }); - it('should show a loading message', async () => { - await expect( - simulator.mapStateTransitions(() => { - return simulator.graphLoadingElement().length; - }) - ).toSometimesYieldEqualTo(1); - }); -}); From 02c68f92675dc41de61811e5873457384a284615 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 28 Jul 2020 12:42:59 -0400 Subject: [PATCH 26/29] change things: * debugActions now shows correct state for each action * change `toSometimesYieldEqualTo` to `toYieldEqualTo` * remove code that tries to show panel * comments --- .../resolver/test_utilities/extend_jest.ts | 47 ++++++++++--------- .../resolver/test_utilities/react_wrapper.ts | 17 +++++++ .../test_utilities/spy_middleware_factory.ts | 12 +++-- .../resolver/view/clickthrough.test.tsx | 4 +- .../resolver/view/process_event_dot.tsx | 2 +- .../view/resolver_without_providers.tsx | 15 +----- 6 files changed, 53 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/test_utilities/react_wrapper.ts diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index f244a6481fdd3c..f5d4a279f1c0f8 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -11,13 +11,12 @@ * order to be correct, the code that the `declare` declares needs to be available on import as well. */ export {}; + declare global { /* eslint-disable @typescript-eslint/no-namespace */ namespace jest { interface Matchers { - toSometimesYieldEqualTo( - expectedYield: T extends AsyncIterable ? E : never - ): Promise; + toYieldEqualTo(expectedYield: T extends AsyncIterable ? E : never): Promise; } } } @@ -28,7 +27,7 @@ expect.extend({ * If any yielded value deep-equals the expected value, the matcher will pass. * If the generator ends with none of the yielded values matching, it will fail. */ - async toSometimesYieldEqualTo( + async toYieldEqualTo( this: jest.MatcherContext, receivedIterable: AsyncIterable, expected: T @@ -41,17 +40,17 @@ expect.extend({ promise: this.promise, }; // The last value received: Used in printing the message - let lastReceived: T | undefined; + const received: T[] = []; // Set to true if the test passes. let pass: boolean = false; // Async iterate over the iterable - for await (const received of receivedIterable) { - // keep track of the last value. Used in both pass and fail messages - lastReceived = received; + for await (const next of receivedIterable) { + // keep track of all received values. Used in pass and fail messages + received.push(next); // Use deep equals to compare the value to the expected value - if (this.equals(received, expected)) { + if (this.equals(next, expected)) { // If the value is equal, break pass = true; break; @@ -64,23 +63,25 @@ expect.extend({ ? () => `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + `Expected: not ${this.utils.printExpected(expected)}\n${ - this.utils.stringify(expected) !== this.utils.stringify(lastReceived!) - ? `Received: ${this.utils.printReceived(lastReceived)}` + this.utils.stringify(expected) !== this.utils.stringify(received!) + ? `Received: ${this.utils.printReceived(received)}` : '' }` : () => - `${this.utils.matcherHint( - matcherName, - undefined, - undefined, - options - )}\n\n${this.utils.printDiffOrStringify( - expected, - lastReceived, - 'Expected', - 'Received', - this.expand - )}`; + `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\nCompared ${ + received.length + } yields.\n\n${received + .map( + (next, index) => + `yield ${index + 1}:\n\n${this.utils.printDiffOrStringify( + expected, + next, + 'Expected', + 'Received', + this.expand + )}` + ) + .join(`\n\n`)}`; return { message, pass }; }, diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/react_wrapper.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/react_wrapper.ts new file mode 100644 index 00000000000000..40267d83c30f84 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/react_wrapper.ts @@ -0,0 +1,17 @@ +/* + * 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 { ReactWrapper } from 'enzyme'; + +/** + * Return a collection of attribute 'entries'. + * The 'entries' are attributeName-attributeValue tuples. + */ +export function attributeEntries(wrapper: ReactWrapper): Array<[string, string]> { + return Array.prototype.slice + .call(wrapper.getDOMNode().attributes) + .map(({ name, value }) => [name, value]); +} diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts index 4e70740df9025e..45730531cf4672 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/spy_middleware_factory.ts @@ -25,25 +25,29 @@ export const spyMiddlewareFactory: () => SpyMiddleware = () => { return { middleware: (api) => (next) => (action: ResolverAction) => { + // handle the action first so we get the state after the reducer + next(action); + const state = api.getState(); + + // Resolving these promises may cause code to await the next result. That will add more resolve functions to `resolvers`. + // For this reason, copy all the existing resolvers to an array and clear the set. const oldResolvers = [...resolvers]; resolvers.clear(); for (const resolve of oldResolvers) { resolve({ action, state }); } - - next(action); }, actions, debugActions() { let stop: boolean = false; (async () => { - for await (const action of actions()) { + for await (const actionStatePair of actions()) { if (stop) { break; } // eslint-disable-next-line no-console - console.log('action', action); + console.log('action', actionStatePair.action, 'state', actionStatePair.state); } })(); return () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 321fb3a458561e..9cb900736677e0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -45,7 +45,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( graphLoadingElements: simulator.graphLoadingElement().length, graphErrorElements: simulator.graphErrorElement().length, })) - ).toSometimesYieldEqualTo({ + ).toYieldEqualTo({ // it should have 1 graph element, an no error or loading elements. graphElements: 1, graphLoadingElements: 0, @@ -85,7 +85,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', ( originLooksUnselected: simulator.processNodeElementLooksUnselected(entityIDs.origin), }; }) - ).toSometimesYieldEqualTo({ + ).toYieldEqualTo({ // Just the second child should be marked as selected in the query string queryStringSelectedNode: [entityIDs.secondChild], // The second child is rendered and has `[aria-selected]` diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 0c7ddc9c108fb9..24de45ee894dcb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -316,7 +316,7 @@ const UnstyledProcessEventDot = React.memo( () => { handleFocus(); handleClick(); - } /* a11y note: this is strictly an alternate to the button, so no tabindex is necessary*/ + } /* a11y note: this is strictly an alternate to the button, so no tabindex is necessary*/ } role="img" aria-labelledby={labelHTMLID} diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 3b28a9ba490435..c5e5ff98ed7ae6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -72,25 +72,12 @@ export const ResolverWithoutProviders = React.memo( const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); - const { - cleanUpQueryParams, - queryParams: { crumbId }, - pushToQueryParams, - } = useResolverQueryParams(); + const { cleanUpQueryParams } = useResolverQueryParams(); useEffectOnce(() => { return () => cleanUpQueryParams(); }); - useEffect(() => { - // When you refresh the page after selecting a process in the table view (not the timeline view) - // The old crumbId still exists in the query string even though a resolver is no longer visible - // This just makes sure the activeDescendant and crumbId are in sync on load for that view as well as the timeline - if (activeDescendantId && crumbId !== activeDescendantId) { - pushToQueryParams({ crumbId: activeDescendantId, crumbEvent: '' }); - } - }, [crumbId, activeDescendantId, pushToQueryParams]); - return ( {isLoading ? ( From 12ef066f6330076ba0d5c5bb55043e20c0e32054 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 28 Jul 2020 14:43:18 -0400 Subject: [PATCH 27/29] fix types --- .../public/resolver/view/resolver_without_providers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index c5e5ff98ed7ae6..f444d5a25e1ef9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -8,7 +8,7 @@ /* eslint-disable react/display-name */ -import React, { useContext, useCallback, useEffect } from 'react'; +import React, { useContext, useCallback } from 'react'; import { useSelector } from 'react-redux'; import { useEffectOnce } from 'react-use'; import { EuiLoadingSpinner } from '@elastic/eui'; From 6f29db49c8b3b3389ed5a30910eed1bae105a481 Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 28 Jul 2020 15:33:15 -0400 Subject: [PATCH 28/29] fix? --- .../public/resolver/test_utilities/extend_jest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index f5d4a279f1c0f8..23a984efa62b23 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -63,8 +63,8 @@ expect.extend({ ? () => `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + `Expected: not ${this.utils.printExpected(expected)}\n${ - this.utils.stringify(expected) !== this.utils.stringify(received!) - ? `Received: ${this.utils.printReceived(received)}` + this.utils.stringify(expected) !== this.utils.stringify(received[0]!) + ? `Received: ${this.utils.printReceived(received[0])}` : '' }` : () => From 9602cc5f1ab7703042c1fe127b636a1d2ba24d9f Mon Sep 17 00:00:00 2001 From: oatkiller Date: Tue, 28 Jul 2020 16:46:02 -0400 Subject: [PATCH 29/29] fixup --- .../public/resolver/test_utilities/extend_jest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts index 23a984efa62b23..9fc7af38beb42a 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/extend_jest.ts @@ -34,7 +34,7 @@ expect.extend({ ): Promise<{ pass: boolean; message: () => string }> { // Used in printing out the pass or fail message const matcherName = 'toSometimesYieldEqualTo'; - const options = { + const options: jest.MatcherHintOptions = { comment: 'deep equality with any yielded value', isNot: this.isNot, promise: this.promise, @@ -63,8 +63,8 @@ expect.extend({ ? () => `${this.utils.matcherHint(matcherName, undefined, undefined, options)}\n\n` + `Expected: not ${this.utils.printExpected(expected)}\n${ - this.utils.stringify(expected) !== this.utils.stringify(received[0]!) - ? `Received: ${this.utils.printReceived(received[0])}` + this.utils.stringify(expected) !== this.utils.stringify(received[received.length - 1]!) + ? `Received: ${this.utils.printReceived(received[received.length - 1])}` : '' }` : () =>