Skip to content

Commit

Permalink
Add simulator test, refactor some selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
kqualters-elastic committed Oct 5, 2020
1 parent 532332d commit ceb4363
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
SafeResolverEvent,
} from '../../../common/endpoint/types';

export const relatedEventsPaginationSize = 25;

/**
* 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.
*/
Expand Down Expand Up @@ -50,7 +52,7 @@ export function dataAccessLayerFactory(
after?: string
): Promise<ResolverPaginatedEvents> {
return context.services.http.post('/api/endpoint/resolver/events', {
query: { afterEvent: after, limit: 25 },
query: { afterEvent: after, limit: relatedEventsPaginationSize },
body: JSON.stringify({
filter: `process.entity_id:"${entityID}" and event.category:"${category}"`,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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';
import { mockTreeWithOneNodeAndTwoPagesOfRelatedEvents } from '../../mocks/resolver_tree';
import { relatedEventsPaginationSize } from '../factory';
import {
ResolverRelatedEvents,
ResolverTree,
ResolverEntityIndex,
SafeResolverEvent,
} from '../../../../common/endpoint/types';
import * as eventModel from '../../../../common/endpoint/models/event';

interface Metadata {
/**
* 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';
};
}
export function oneNodeWithPaginatedEvents(): {
dataAccessLayer: DataAccessLayer;
metadata: Metadata;
} {
const metadata: Metadata = {
databaseDocumentID: '_id',
entityIDs: { origin: 'origin' },
};
const tree = mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({
originID: metadata.entityIDs.origin,
});

return {
metadata,
dataAccessLayer: {
/**
* Fetch related events for an entity ID
*/
async relatedEvents(entityID: string): Promise<ResolverRelatedEvents> {
/**
* Respond with the mocked related events when the origin's related events are fetched.
**/
const events = entityID === metadata.entityIDs.origin ? tree.relatedEvents.events : [];

return {
entityID,
events,
nextEvent: null,
};
},

/**
* If called with an "after" cursor, return the 2nd page, else return the first.
*/
async eventsWithEntityIDAndCategory(
entityID: string,
category: string,
after?: string
): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> {
let events: SafeResolverEvent[] = [];
const eventsOfCategory = tree.relatedEvents.events.filter(
(event) => event.event?.category === category
);
if (after === undefined) {
events = eventsOfCategory.slice(0, relatedEventsPaginationSize);
} else {
events = eventsOfCategory.slice(relatedEventsPaginationSize - 1);
}
return {
events,
nextEvent: typeof after === 'undefined' ? 'firstEventPage2' : null,
};
},

/**
* Any of the origin's related events by event.id
*/
async event(eventID: string): Promise<SafeResolverEvent | null> {
return (
tree.relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null
);
},

/**
* Fetch a ResolverTree for a entityID
*/
async resolverTree(): Promise<ResolverTree> {
return tree;
},

/**
* Get entities matching a document.
*/
async entities(): Promise<ResolverEntityIndex> {
return [{ entity_id: metadata.entityIDs.origin }];
},
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,49 @@

import { mockEndpointEvent } from './endpoint_event';
import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types';
import { relatedEventsPaginationSize } from '../data_access_layer/factory';
import * as eventModel from '../../../common/endpoint/models/event';

export function mockTreeWithOneNodeAndTwoPagesOfRelatedEvents({
originID,
}: {
originID: string;
}): ResolverTree {
const originEvent: SafeResolverEvent = mockEndpointEvent({
entityID: originID,
processName: 'c',
parentEntityID: undefined,
timestamp: 1600863932318,
});
const events = [];
const eventsToGenerate = relatedEventsPaginationSize + 5;
for (let i = 0; i < eventsToGenerate; i++) {
const newEvent = mockEndpointEvent({
entityID: originID,
eventID: 'test',
eventType: 'access',
eventCategory: 'registry',
timestamp: 1600863932318,
});
events.push(newEvent);
}
return {
entityID: originID,
children: {
childNodes: [],
nextChild: null,
},
ancestry: {
nextAncestor: null,
ancestors: [],
},
lifecycle: [originEvent],
relatedEvents: { events, nextEvent: null },
relatedAlerts: { alerts: [], nextAlert: null },
stats: { events: { total: eventsToGenerate, byCategory: {} }, totalAlerts: 0 },
};
}

export function mockTreeWith2AncestorsAndNoChildren({
originID,
firstAncestorID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,22 @@ interface UserRequestedAdditionalRelatedEvents {
readonly payload: {};
}

interface AppRequestedAdditionalRelatedEvents {
readonly type: 'appRequestedAdditionalRelatedEvents';
readonly payload: {};
}

interface ServerFailedToReturnNodeEventsInCategory {
readonly type: 'serverFailedToReturnNodeEventsInCategory';
}

/**
* When an additional page of related events is returned
*/
interface ServerReturnedAdditionalRelatedEventData {
readonly type: 'serverReturnedAdditionalRelatedEventData';
readonly payload: ResolverRelatedEvents;
}

interface ServerFailedToReturnAdditionalRelatedEventData {
readonly type: 'serverFailedToReturnAdditionalRelatedEventData';
/**
* entity ID used to make the failed request
*/
readonly payload: TreeFetcherParameters;
readonly payload: {
/**
* The cursor, if any, that can be used to retrieve more events.
*/
cursor: string | null;
/**
* The nodeID that `events` are related to.
*/
nodeID: string;
/**
* The category that `events` have in common.
*/
eventCategory: string;
};
}

interface ServerFailedToReturnResolverData {
Expand Down Expand Up @@ -139,8 +132,5 @@ export type DataAction =
| AppRequestedResolverData
| AppRequestedRelatedEventData
| UserRequestedAdditionalRelatedEvents
| AppRequestedAdditionalRelatedEvents
| ServerFailedToReturnAdditionalRelatedEventData
| ServerReturnedAdditionalRelatedEventData
| ServerFailedToReturnNodeEventsInCategory
| AppAbortedResolverDataRequest;
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function updatedWith(
eventCategory: first.eventCategory,
events: [...first.events, ...second.events],
cursor: second.cursor,
lastCursorRequested: null,
};
} else {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
// the action is stale, ignore it
return state;
}
} else if (action.type === 'appRequestedAdditionalRelatedEvents') {
} else if (action.type === 'userRequestedAdditionalRelatedEvents') {
const nextState: DataState = {
...state,
nodeEventsInCategory: {
Expand All @@ -173,6 +173,20 @@ export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialS
cursor: null,
...state.nodeEventsInCategory,
loading: true,
lastCursorRequested: state.nodeEventsInCategory?.cursor,
},
};
return nextState;
} else if (action.type === 'serverFailedToReturnNodeEventsInCategory') {
const nextState: DataState = {
...state,
nodeEventsInCategory: {
nodeID: '',
eventCategory: '',
events: [],
cursor: null,
...state.nodeEventsInCategory,
error: true,
},
};
return nextState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
import { isGraphableProcess, isTerminatedProcess } from '../../models/process_event';
import * as indexedProcessTreeModel from '../../models/indexed_process_tree';
import * as eventModel from '../../../../common/endpoint/models/event';

import * as nodeEventsInCategoryModel from './node_events_in_category_model';
import {
ResolverTree,
ResolverNodeStats,
Expand Down Expand Up @@ -666,10 +666,73 @@ export const nodeEventsInCategory = (state: DataState) => {
return state.nodeEventsInCategory?.events ?? [];
};

export const nodeEventsInCategoryAreLoading = (state: DataState) => {
return state.nodeEventsInCategory?.loading ?? false;
};
export const userCanRequestMoreNodeEventsInCategory = createSelector(
(state: DataState) => state.nodeEventsInCategory,
panelViewAndParameters,
/* eslint-disable-next-line no-shadow */
function (nodeEventsInCategory, panelViewAndParameters) {
if (
nodeEventsInCategory !== undefined &&
nodeEventsInCategoryModel.isRelevantToPanelViewAndParameters(
nodeEventsInCategory,
panelViewAndParameters
)
) {
return nodeEventsInCategory.cursor !== null;
} else {
return false;
}
}
);

export const nodeEventsInCategoryNextCursor = (state: DataState) => {
return state.nodeEventsInCategory?.cursor ?? null;
};
export const hadErrorLoadingNodeEventsInCategory = createSelector(
(state: DataState) => state.nodeEventsInCategory,
panelViewAndParameters,
/* eslint-disable-next-line no-shadow */
function (nodeEventsInCategory, panelViewAndParameters) {
if (
nodeEventsInCategory !== undefined &&
nodeEventsInCategoryModel.isRelevantToPanelViewAndParameters(
nodeEventsInCategory,
panelViewAndParameters
)
) {
return nodeEventsInCategory && nodeEventsInCategory.error === true;
} else {
return false;
}
}
);

export const isLoadingNodeEventsInCategory = createSelector(
(state: DataState) => state.nodeEventsInCategory,
panelViewAndParameters,
/* eslint-disable-next-line no-shadow */
function (nodeEventsInCategory, panelViewAndParameters) {
const { panelView } = panelViewAndParameters;
return panelView === 'nodeEventsInCategory' && nodeEventsInCategory === undefined;
}
);

export const isLoadingMoreNodeEventsInCategory = createSelector(
(state: DataState) => state.nodeEventsInCategory,
panelViewAndParameters,
/* eslint-disable-next-line no-shadow */
function (nodeEventsInCategory, panelViewAndParameters) {
if (
nodeEventsInCategory !== undefined &&
nodeEventsInCategoryModel.isRelevantToPanelViewAndParameters(
nodeEventsInCategory,
panelViewAndParameters
)
) {
return (
nodeEventsInCategory &&
nodeEventsInCategory.lastCursorRequested !== null &&
nodeEventsInCategory.cursor !== nodeEventsInCategory.lastCursorRequested
);
} else {
return false;
}
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function RelatedEventsFetcher(
eventCategory: newParams.panelParameters.eventCategory,
cursor: result.nextEvent,
nodeID,
lastCursorRequested: null,
},
});
}
Expand All @@ -69,10 +70,6 @@ export function RelatedEventsFetcher(
} else if (action.type === 'userRequestedAdditionalRelatedEvents') {
const nodeEventsInCategory = state.data.nodeEventsInCategory;
if (nodeEventsInCategory !== undefined) {
api.dispatch({
type: 'appRequestedAdditionalRelatedEvents',
payload: {},
});
const { nodeID, eventCategory, cursor } = nodeEventsInCategory;
let result: ResolverPaginatedEvents | null = null;
try {
Expand All @@ -88,6 +85,11 @@ export function RelatedEventsFetcher(
} catch (error) {
api.dispatch({
type: 'serverFailedToReturnNodeEventsInCategory',
payload: {
nodeID,
eventCategory,
cursor,
},
});
}

Expand Down
Loading

0 comments on commit ceb4363

Please sign in to comment.