@cccsaurora/howler-ui 2.19.0-cases.1095 → 2.19.0-cases.1097
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Hoisted mocks
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const mockHitCardProps = vi.hoisted(() => ({ current: [] }));
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Module mocks
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
vi.mock('components/elements/hit/HitCard', () => ({
|
|
14
|
+
default: (props) => {
|
|
15
|
+
mockHitCardProps.current.push(props);
|
|
16
|
+
return _jsx("div", { id: `hit-card-${String(props.id)}` });
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('react-i18next', () => ({
|
|
20
|
+
useTranslation: () => ({
|
|
21
|
+
t: (key) => key
|
|
22
|
+
})
|
|
23
|
+
}));
|
|
24
|
+
vi.mock('react-router-dom', async () => {
|
|
25
|
+
const actual = await vi.importActual('react-router-dom');
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
Link: ({ to, children, ...props }) => (_jsx("a", { href: to, ...props, children: children }))
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Imports after mocks
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
import AlertPanel from './AlertPanel';
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
const renderPanel = (caseValue) => {
|
|
39
|
+
return render(_jsx(MemoryRouter, { children: _jsx(AlertPanel, { case: caseValue }) }));
|
|
40
|
+
};
|
|
41
|
+
const makeHitItem = (id, path = `/cases/test/${id}`) => ({
|
|
42
|
+
type: 'hit',
|
|
43
|
+
value: id,
|
|
44
|
+
path
|
|
45
|
+
});
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Setup
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
mockHitCardProps.current = [];
|
|
51
|
+
});
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Tests
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
describe('AlertPanel', () => {
|
|
56
|
+
it('renders a skeleton when the case is null', () => {
|
|
57
|
+
const { container } = renderPanel(null);
|
|
58
|
+
expect(container.querySelector('.MuiSkeleton-root')).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
it('renders the translated heading key', () => {
|
|
61
|
+
const _case = createMockCase({ case_id: 'case-1', items: [] });
|
|
62
|
+
renderPanel(_case);
|
|
63
|
+
expect(screen.getByText('page.cases.dashboard.alerts')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
it('renders HitCard only for unique hit items on the current page', () => {
|
|
66
|
+
const duplicate = makeHitItem('hit-1', '/cases/test/path-a');
|
|
67
|
+
const _case = createMockCase({
|
|
68
|
+
case_id: 'case-2',
|
|
69
|
+
items: [
|
|
70
|
+
duplicate,
|
|
71
|
+
duplicate,
|
|
72
|
+
makeHitItem('hit-2', '/cases/test/path-b'),
|
|
73
|
+
{ type: 'event', value: 'event-1', path: '/cases/test/event-1' }
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
renderPanel(_case);
|
|
77
|
+
expect(screen.getByTestId('hit-card-hit-1')).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByTestId('hit-card-hit-2')).toBeInTheDocument();
|
|
79
|
+
expect(screen.queryByTestId('hit-card-event-1')).not.toBeInTheDocument();
|
|
80
|
+
expect(mockHitCardProps.current).toHaveLength(2);
|
|
81
|
+
expect(mockHitCardProps.current[0]).toEqual(expect.objectContaining({
|
|
82
|
+
id: 'hit-1',
|
|
83
|
+
lazy: true,
|
|
84
|
+
layout: 'dense'
|
|
85
|
+
}));
|
|
86
|
+
});
|
|
87
|
+
it('renders overlay links that target each hit path', () => {
|
|
88
|
+
const _case = createMockCase({
|
|
89
|
+
case_id: 'case-3',
|
|
90
|
+
items: [makeHitItem('hit-1', '/cases/case-3/path-one'), makeHitItem('hit-2', '/cases/case-3/path-two')]
|
|
91
|
+
});
|
|
92
|
+
const { container } = renderPanel(_case);
|
|
93
|
+
const links = Array.from(container.querySelectorAll('a'));
|
|
94
|
+
expect(links).toHaveLength(2);
|
|
95
|
+
expect(links[0]).toHaveAttribute('href', '/cases/case-3/path-one');
|
|
96
|
+
expect(links[1]).toHaveAttribute('href', '/cases/case-3/path-two');
|
|
97
|
+
});
|
|
98
|
+
it('shows pagination with multiple pages and switches to page 2 items', () => {
|
|
99
|
+
const items = Array.from({ length: 6 }, (_, idx) => makeHitItem(`hit-${idx + 1}`, `/cases/test/hit-${idx + 1}`));
|
|
100
|
+
const _case = createMockCase({ case_id: 'case-4', items });
|
|
101
|
+
renderPanel(_case);
|
|
102
|
+
expect(screen.getByTestId('hit-card-hit-1')).toBeInTheDocument();
|
|
103
|
+
expect(screen.getByTestId('hit-card-hit-5')).toBeInTheDocument();
|
|
104
|
+
expect(screen.queryByTestId('hit-card-hit-6')).not.toBeInTheDocument();
|
|
105
|
+
const pageTwoButton = screen.getByRole('button', { name: 'Go to page 2' });
|
|
106
|
+
fireEvent.click(pageTwoButton);
|
|
107
|
+
expect(screen.getByTestId('hit-card-hit-6')).toBeInTheDocument();
|
|
108
|
+
expect(screen.queryByTestId('hit-card-hit-1')).not.toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { createMockCase, createMockEvent, createMockHit } from '@cccsaurora/howler-ui/tests/utils';
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Hoisted mocks
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const mockDispatchApi = vi.hoisted(() => vi.fn());
|
|
11
|
+
const mockSearchPost = vi.hoisted(() => vi.fn());
|
|
12
|
+
const mockUpdateCase = vi.hoisted(() => vi.fn());
|
|
13
|
+
const mockUseCaseState = vi.hoisted(() => ({ case: null }));
|
|
14
|
+
const mockAggregateProps = vi.hoisted(() => ({
|
|
15
|
+
current: []
|
|
16
|
+
}));
|
|
17
|
+
const mockCaseOverviewProps = vi.hoisted(() => ({ current: [] }));
|
|
18
|
+
const mockTaskPanelProps = vi.hoisted(() => ({ current: [] }));
|
|
19
|
+
const mockAlertPanelProps = vi.hoisted(() => ({ current: [] }));
|
|
20
|
+
const mockRelatedCasePanelProps = vi.hoisted(() => ({ current: [] }));
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Module mocks
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
vi.mock('use-context-selector', async () => {
|
|
25
|
+
const react = (await vi.importActual('react'));
|
|
26
|
+
return {
|
|
27
|
+
createContext: react.createContext,
|
|
28
|
+
useContextSelector: (context, selector) => {
|
|
29
|
+
return selector(react.useContext(context));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
vi.mock('components/hooks/useMyApi', () => ({
|
|
34
|
+
default: () => ({ dispatchApi: mockDispatchApi })
|
|
35
|
+
}));
|
|
36
|
+
vi.mock('api', () => ({
|
|
37
|
+
default: {
|
|
38
|
+
v2: {
|
|
39
|
+
search: {
|
|
40
|
+
post: (...args) => mockSearchPost(...args)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
vi.mock('../hooks/useCase', () => ({
|
|
46
|
+
default: () => ({
|
|
47
|
+
case: mockUseCaseState.case,
|
|
48
|
+
update: mockUpdateCase
|
|
49
|
+
})
|
|
50
|
+
}));
|
|
51
|
+
vi.mock('react-i18next', () => ({
|
|
52
|
+
useTranslation: () => ({
|
|
53
|
+
t: (key) => key
|
|
54
|
+
})
|
|
55
|
+
}));
|
|
56
|
+
vi.mock('react-router-dom', async () => {
|
|
57
|
+
const actual = await vi.importActual('react-router-dom');
|
|
58
|
+
return {
|
|
59
|
+
...actual,
|
|
60
|
+
useOutletContext: () => createMockCase({ case_id: 'route-case-id', items: [] })
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
vi.mock('./aggregates/CaseAggregate', () => ({
|
|
64
|
+
default: (props) => {
|
|
65
|
+
mockAggregateProps.current.push(props);
|
|
66
|
+
return _jsx("div", { id: `aggregate-${String(mockAggregateProps.current.length)}` });
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
vi.mock('./CaseOverview', () => ({
|
|
70
|
+
default: (props) => {
|
|
71
|
+
mockCaseOverviewProps.current.push(props);
|
|
72
|
+
return _jsx("div", { id: "case-overview" });
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
vi.mock('./TaskPanel', () => ({
|
|
76
|
+
default: (props) => {
|
|
77
|
+
mockTaskPanelProps.current.push(props);
|
|
78
|
+
return _jsx("div", { id: "task-panel" });
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
vi.mock('./AlertPanel', () => ({
|
|
82
|
+
default: (props) => {
|
|
83
|
+
mockAlertPanelProps.current.push(props);
|
|
84
|
+
return _jsx("div", { id: "alert-panel" });
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
vi.mock('./RelatedCasePanel', () => ({
|
|
88
|
+
default: (props) => {
|
|
89
|
+
mockRelatedCasePanelProps.current.push(props);
|
|
90
|
+
return _jsx("div", { id: "related-case-panel" });
|
|
91
|
+
}
|
|
92
|
+
}));
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Imports after mocks
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
import CaseDashboard from './CaseDashboard';
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Helpers
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
const makeRecordContextValue = (records, loadRecords) => ({
|
|
101
|
+
records,
|
|
102
|
+
selectedRecords: [],
|
|
103
|
+
addRecordToSelection: vi.fn(),
|
|
104
|
+
removeRecordFromSelection: vi.fn(),
|
|
105
|
+
clearSelectedRecords: vi.fn(),
|
|
106
|
+
loadRecords,
|
|
107
|
+
updateRecord: vi.fn(),
|
|
108
|
+
getRecord: vi.fn()
|
|
109
|
+
});
|
|
110
|
+
const renderDashboard = ({ dashboardCase, initialRecords = {}, onLoadRecords }) => {
|
|
111
|
+
const Wrapper = ({ children }) => {
|
|
112
|
+
const [records, setRecords] = useState(initialRecords);
|
|
113
|
+
const loadRecords = items => {
|
|
114
|
+
onLoadRecords?.(items);
|
|
115
|
+
const mapped = Object.fromEntries(items.map(item => [item.howler.id, item]));
|
|
116
|
+
setRecords(prev => ({ ...prev, ...mapped }));
|
|
117
|
+
};
|
|
118
|
+
return (_jsx(RecordContext.Provider, { value: makeRecordContextValue(records, loadRecords), children: children }));
|
|
119
|
+
};
|
|
120
|
+
return render(_jsx(CaseDashboard, { case: dashboardCase }), { wrapper: Wrapper });
|
|
121
|
+
};
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Setup
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
mockDispatchApi.mockReset();
|
|
127
|
+
mockSearchPost.mockReset();
|
|
128
|
+
mockUpdateCase.mockReset();
|
|
129
|
+
mockAggregateProps.current = [];
|
|
130
|
+
mockCaseOverviewProps.current = [];
|
|
131
|
+
mockTaskPanelProps.current = [];
|
|
132
|
+
mockAlertPanelProps.current = [];
|
|
133
|
+
mockRelatedCasePanelProps.current = [];
|
|
134
|
+
mockUseCaseState.case = null;
|
|
135
|
+
});
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Tests
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
describe('CaseDashboard', () => {
|
|
140
|
+
it('renders nothing when useCase does not return a case', () => {
|
|
141
|
+
const baseCase = createMockCase({ case_id: 'case-1', items: [] });
|
|
142
|
+
mockUseCaseState.case = null;
|
|
143
|
+
const { container } = renderDashboard({ dashboardCase: baseCase });
|
|
144
|
+
expect(container).toBeEmptyDOMElement();
|
|
145
|
+
});
|
|
146
|
+
it('renders overview, panels and aggregate cards with translated subtitles', () => {
|
|
147
|
+
const dashboardCase = createMockCase({
|
|
148
|
+
case_id: 'case-2',
|
|
149
|
+
items: [
|
|
150
|
+
{ type: 'hit', value: 'hit-1', path: 'root/hit-1' },
|
|
151
|
+
{ type: 'event', value: 'event-1', path: 'root/event-1' }
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
mockUseCaseState.case = dashboardCase;
|
|
155
|
+
renderDashboard({
|
|
156
|
+
dashboardCase,
|
|
157
|
+
initialRecords: {
|
|
158
|
+
'hit-1': createMockHit({ howler: { id: 'hit-1', outline: { threat: 'threat-a', target: 'target-a' } } }),
|
|
159
|
+
'event-1': createMockEvent({ howler: { id: 'event-1', outline: { indicators: ['ioc-a'] } } })
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
expect(mockCaseOverviewProps.current).toHaveLength(1);
|
|
163
|
+
expect(mockTaskPanelProps.current).toHaveLength(1);
|
|
164
|
+
expect(mockAlertPanelProps.current).toHaveLength(1);
|
|
165
|
+
expect(mockRelatedCasePanelProps.current).toHaveLength(1);
|
|
166
|
+
expect(mockAggregateProps.current).toHaveLength(4);
|
|
167
|
+
expect(mockAggregateProps.current[0]).toEqual(expect.objectContaining({
|
|
168
|
+
field: 'howler.outline.threat',
|
|
169
|
+
subtitle: 'page.cases.dashboard.threat'
|
|
170
|
+
}));
|
|
171
|
+
expect(mockAggregateProps.current[1]).toEqual(expect.objectContaining({
|
|
172
|
+
field: 'howler.outline.target',
|
|
173
|
+
subtitle: 'page.cases.dashboard.target'
|
|
174
|
+
}));
|
|
175
|
+
expect(mockAggregateProps.current[2]).toEqual(expect.objectContaining({
|
|
176
|
+
field: 'howler.outline.indicators',
|
|
177
|
+
subtitle: 'page.cases.dashboard.indicators'
|
|
178
|
+
}));
|
|
179
|
+
expect(mockAggregateProps.current[3]).toEqual(expect.objectContaining({
|
|
180
|
+
subtitle: 'page.cases.dashboard.duration',
|
|
181
|
+
title: '--'
|
|
182
|
+
}));
|
|
183
|
+
});
|
|
184
|
+
it('loads missing hit and event records and sends expected search query', async () => {
|
|
185
|
+
const dashboardCase = createMockCase({
|
|
186
|
+
case_id: 'case-3',
|
|
187
|
+
items: [
|
|
188
|
+
{ type: 'hit', value: 'hit-1', path: 'root/hit-1' },
|
|
189
|
+
{ type: 'event', value: 'event-1', path: 'root/event-1' },
|
|
190
|
+
{ type: 'hit', value: 'hit-2', path: 'root/hit-2' },
|
|
191
|
+
{ type: 'case', value: 'case-child', path: 'root/case-child' }
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
mockUseCaseState.case = dashboardCase;
|
|
195
|
+
const loadedEvent = createMockEvent({ howler: { id: 'event-1' } });
|
|
196
|
+
const loadedHit = createMockHit({ howler: { id: 'hit-2' } });
|
|
197
|
+
mockSearchPost.mockReturnValue({ endpoint: 'search' });
|
|
198
|
+
mockDispatchApi.mockResolvedValue({
|
|
199
|
+
items: [loadedEvent, loadedHit]
|
|
200
|
+
});
|
|
201
|
+
const onLoadRecords = vi.fn();
|
|
202
|
+
renderDashboard({
|
|
203
|
+
dashboardCase,
|
|
204
|
+
initialRecords: {
|
|
205
|
+
'hit-1': createMockHit({ howler: { id: 'hit-1' } })
|
|
206
|
+
},
|
|
207
|
+
onLoadRecords
|
|
208
|
+
});
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
expect(mockSearchPost).toHaveBeenCalledWith(['hit', 'event'], {
|
|
211
|
+
query: 'howler.id:(event-1 OR hit-2)',
|
|
212
|
+
metadata: ['template', 'analytic']
|
|
213
|
+
});
|
|
214
|
+
expect(mockDispatchApi).toHaveBeenCalledTimes(1);
|
|
215
|
+
expect(onLoadRecords).toHaveBeenCalledWith([loadedEvent, loadedHit]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|