Add scan execution policies to the policy list

The policy list now also fetches scan execution policies and displays
them along with network policies.
This also renames a few things to better reflect the fact that we no
longer display network policies only.
The specs have been reorganized and cleaned up to make sure we only test
relevant things, that no test-cases are duplicated, and that we only
create a single wrapper per test case. We also now mount shallow
wrappers when a full mount isn't necessary.

Changelog: added
EE: true
parent 8c59237a
......@@ -3,8 +3,8 @@ import { GlIcon, GlLink, GlPopover, GlTabs, GlTab } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { s__ } from '~/locale';
import Alerts from './alerts/alerts.vue';
import NetworkPolicyList from './network_policy_list.vue';
import NoEnvironmentEmptyState from './no_environment_empty_state.vue';
import PolicyList from './policy_list.vue';
import ThreatMonitoringFilters from './threat_monitoring_filters.vue';
import ThreatMonitoringSection from './threat_monitoring_section.vue';
......@@ -19,7 +19,7 @@ export default {
Alerts,
ThreatMonitoringFilters,
ThreatMonitoringSection,
NetworkPolicyList,
PolicyList,
NoEnvironmentEmptyState,
},
inject: ['documentationPath'],
......@@ -94,9 +94,9 @@ export default {
>
<alerts />
</gl-tab>
<gl-tab ref="networkPolicyTab" :title="s__('ThreatMonitoring|Policies')">
<gl-tab ref="policyTab" :title="s__('ThreatMonitoring|Policies')">
<no-environment-empty-state v-if="!isSetUpMaybe" />
<network-policy-list
<policy-list
v-else
:documentation-path="documentationPath"
:new-policy-path="newPolicyPath"
......@@ -111,7 +111,7 @@ export default {
<threat-monitoring-filters />
<threat-monitoring-section
ref="networkPolicySection"
ref="policySection"
store-namespace="threatMonitoringNetworkPolicy"
:title="s__('ThreatMonitoring|Container Network Policy')"
:subtitle="s__('ThreatMonitoring|Packet Activity')"
......
......@@ -7,9 +7,20 @@ import { getTimeago } from '~/lib/utils/datetime_utility';
import { setUrlFragment, mergeUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import networkPoliciesQuery from '../graphql/queries/network_policies.query.graphql';
import scanExecutionPoliciesQuery from '../graphql/queries/scan_execution_policies.query.graphql';
import EnvironmentPicker from './environment_picker.vue';
import PolicyDrawer from './policy_drawer/policy_drawer.vue';
const createPolicyFetchError = ({ gqlError, networkError }) => {
const error =
gqlError?.message ||
networkError?.message ||
s__('NetworkPolicies|Something went wrong, unable to fetch policies');
createFlash({
message: error,
});
};
export default {
components: {
GlTable,
......@@ -48,19 +59,23 @@ export default {
);
return [...policies, ...predefined];
},
error({ gqlError, networkError }) {
const error =
gqlError?.message ||
networkError?.message ||
s__('NetworkPolicies|Something went wrong, unable to fetch policies');
createFlash({
message: error,
});
},
error: createPolicyFetchError,
skip() {
return this.isLoadingEnvironments;
},
},
scanExecutionPolicies: {
query: scanExecutionPoliciesQuery,
variables() {
return {
fullPath: this.projectPath,
};
},
update(data) {
return data?.project?.scanExecutionPolicies?.nodes ?? [];
},
error: createPolicyFetchError,
},
},
data() {
return { selectedPolicyName: null, initialManifest: null, initialEnforcementStatus: null };
......@@ -75,8 +90,15 @@ export default {
documentationFullPath() {
return setUrlFragment(this.documentationPath, 'container-network-policy');
},
policies() {
return [...(this.networkPolicies || []), ...(this.scanExecutionPolicies || [])];
},
isLoadingPolicies() {
return this.isLoadingEnvironments || this.$apollo.queries.networkPolicies.loading;
return (
this.isLoadingEnvironments ||
this.$apollo.queries.networkPolicies.loading ||
this.$apollo.queries.scanExecutionPolicies.loading
);
},
hasSelectedPolicy() {
return Boolean(this.selectedPolicyName);
......@@ -188,7 +210,7 @@ export default {
<gl-table
ref="policiesTable"
:busy="isLoadingPolicies"
:items="networkPolicies"
:items="policies"
:fields="fields"
head-variant="white"
stacked="md"
......
query scanExecutionPolicies($fullPath: ID!) {
project(fullPath: $fullPath) {
scanExecutionPolicies {
nodes {
name
yaml
enabled
updatedAt
}
}
}
}
......@@ -30,7 +30,6 @@ export default () => {
const {
networkPolicyStatisticsEndpoint,
environmentsEndpoint,
networkPoliciesEndpoint,
emptyStateSvgPath,
networkPolicyNoDataSvgPath,
newPolicyPath,
......@@ -44,9 +43,6 @@ export default () => {
networkPolicyStatisticsEndpoint,
environmentsEndpoint,
});
store.dispatch('networkPolicies/setEndpoints', {
networkPoliciesEndpoint,
});
return new Vue({
apolloProvider,
......
......@@ -11,7 +11,6 @@
network_policy_no_data_svg_path: image_path('illustrations/network-policies-not-detected-sm.svg'),
network_policy_statistics_endpoint: summary_project_security_network_policies_path(@project, format: :json),
environments_endpoint: project_environments_path(@project),
network_policies_endpoint: project_security_network_policies_path(@project),
new_policy_path: new_project_threat_monitoring_policy_path(@project),
default_environment_id: default_environment_id,
project_path: @project.full_path,
......
......@@ -21,7 +21,7 @@ exports[`ThreatMonitoringApp component given there is a default environment with
title="Policies"
titlelinkclass=""
>
<network-policy-list-stub
<policy-list-stub
documentationpath="/docs"
newpolicypath="/policy/new"
/>
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NetworkPolicyList component given there is a default environment with no data to display shows the table empty state 1`] = `undefined`;
exports[`NetworkPolicyList component renders policies table 1`] = `
<table
aria-busy="false"
aria-colcount="3"
aria-describedby="__BVID__243__caption_"
aria-multiselectable="false"
class="table b-table gl-table table-hover b-table-stacked-md b-table-selectable b-table-select-single"
id="__BVID__243"
role="table"
>
<!---->
<!---->
<thead
class="thead-white gl-text-gray-900 border-bottom"
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<th
aria-colindex="1"
class="gl-w-half"
role="columnheader"
scope="col"
>
<div>
Name
</div>
</th>
<th
aria-colindex="2"
class=""
role="columnheader"
scope="col"
>
<div>
Status
</div>
</th>
<th
aria-colindex="3"
class=""
role="columnheader"
scope="col"
>
<div>
Last modified
</div>
</th>
</tr>
</thead>
<tbody
class="gl-text-gray-900"
role="rowgroup"
>
<!---->
<tr
aria-selected="false"
class=""
role="row"
tabindex="0"
>
<td
aria-colindex="1"
class=""
data-label="Name"
role="cell"
>
<div>
policy
</div>
</td>
<td
aria-colindex="2"
class=""
data-label="Status"
role="cell"
>
<div>
Enabled
</div>
</td>
<td
aria-colindex="3"
class=""
data-label="Last modified"
role="cell"
>
<div>
2 months ago
</div>
</td>
</tr>
<tr
aria-selected="false"
class=""
role="row"
tabindex="0"
>
<td
aria-colindex="1"
class=""
data-label="Name"
role="cell"
>
<div>
drop-outbound
</div>
</td>
<td
aria-colindex="2"
class=""
data-label="Status"
role="cell"
>
<div>
Disabled
</div>
</td>
<td
aria-colindex="3"
class=""
data-label="Last modified"
role="cell"
>
<div>
</div>
</td>
</tr>
<tr
aria-selected="false"
class=""
role="row"
tabindex="0"
>
<td
aria-colindex="1"
class=""
data-label="Name"
role="cell"
>
<div>
allow-inbound-http
</div>
</td>
<td
aria-colindex="2"
class=""
data-label="Status"
role="cell"
>
<div>
Disabled
</div>
</td>
<td
aria-colindex="3"
class=""
data-label="Last modified"
role="cell"
>
<div>
</div>
</td>
</tr>
<!---->
<!---->
</tbody>
<!---->
</table>
`;
import { shallowMount } from '@vue/test-utils';
import ThreatMonitoringAlerts from 'ee/threat_monitoring/components/alerts/alerts.vue';
import ThreatMonitoringApp from 'ee/threat_monitoring/components/app.vue';
import NetworkPolicyList from 'ee/threat_monitoring/components/network_policy_list.vue';
import NoEnvironmentEmptyState from 'ee/threat_monitoring/components/no_environment_empty_state.vue';
import PolicyList from 'ee/threat_monitoring/components/policy_list.vue';
import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue';
import createStore from 'ee/threat_monitoring/store';
import { TEST_HOST } from 'helpers/test_constants';
......@@ -50,11 +50,11 @@ describe('ThreatMonitoringApp component', () => {
};
const findAlertsView = () => wrapper.find(ThreatMonitoringAlerts);
const findNetworkPolicyList = () => wrapper.find(NetworkPolicyList);
const findPolicyList = () => wrapper.find(PolicyList);
const findFilters = () => wrapper.find(ThreatMonitoringFilters);
const findNetworkPolicySection = () => wrapper.find({ ref: 'networkPolicySection' });
const findPolicySection = () => wrapper.find({ ref: 'policySection' });
const findNoEnvironmentEmptyStates = () => wrapper.findAll(NoEnvironmentEmptyState);
const findNetworkPolicyTab = () => wrapper.find({ ref: 'networkPolicyTab' });
const findPolicyTab = () => wrapper.find({ ref: 'policyTab' });
const findAlertTab = () => wrapper.findByTestId('threat-monitoring-alerts-tab');
const findStatisticsTab = () => wrapper.findByTestId('threat-monitoring-statistics-tab');
......@@ -84,16 +84,16 @@ describe('ThreatMonitoringApp component', () => {
});
it('shows the tabs', () => {
expect(findNetworkPolicyTab().exists()).toBe(true);
expect(findPolicyTab().exists()).toBe(true);
expect(findStatisticsTab().exists()).toBe(true);
});
it('does not show the network policy list', () => {
expect(findNetworkPolicyList().exists()).toBe(false);
expect(findPolicyList().exists()).toBe(false);
});
it('does not show the threat monitoring section', () => {
expect(findNetworkPolicySection().exists()).toBe(false);
expect(findPolicySection().exists()).toBe(false);
});
},
);
......@@ -115,11 +115,11 @@ describe('ThreatMonitoringApp component', () => {
});
it('renders the network policy section', () => {
expect(findNetworkPolicySection().element).toMatchSnapshot();
expect(findPolicySection().element).toMatchSnapshot();
});
it('renders the network policy tab', () => {
expect(findNetworkPolicyTab().element).toMatchSnapshot();
expect(findPolicyTab().element).toMatchSnapshot();
});
});
......
import { GlTable, GlDrawer } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import { merge, cloneDeep } from 'lodash';
import VueApollo from 'vue-apollo';
import NetworkPolicyList from 'ee/threat_monitoring/components/network_policy_list.vue';
import PolicyList from 'ee/threat_monitoring/components/policy_list.vue';
import networkPoliciesQuery from 'ee/threat_monitoring/graphql/queries/network_policies.query.graphql';
import scanExecutionPoliciesQuery from 'ee/threat_monitoring/graphql/queries/scan_execution_policies.query.graphql';
import createStore from 'ee/threat_monitoring/store';
import createMockApolloProvider from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { networkPolicies } from '../mocks/mock_apollo';
import { mockPoliciesResponse, mockCiliumPolicy } from '../mocks/mock_data';
import { networkPolicies, scanExecutionPolicies } from '../mocks/mock_apollo';
import { mockPoliciesResponse, mockScanExecutionPolicy } from '../mocks/mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
......@@ -21,16 +24,18 @@ const environments = [
];
const defaultRequestHandlers = {
networkPolicies: networkPolicies(mockPoliciesResponse),
scanExecutionPolicies: scanExecutionPolicies([mockScanExecutionPolicy]),
};
const pendingHandler = jest.fn(() => new Promise(() => {}));
describe('NetworkPolicyList component', () => {
describe('PolicyList component', () => {
let store;
let wrapper;
let requestHandlers;
const factory = ({ mountFn = mountExtended, propsData, state, data, handlers } = {}) => {
const factory = (mountFn = mountExtended) => (options = {}) => {
store = createStore();
const { state, handlers, ...wrapperOptions } = options;
Object.assign(store.state.networkPolicies, {
...state,
});
......@@ -42,96 +47,129 @@ describe('NetworkPolicyList component', () => {
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = mountFn(NetworkPolicyList, {
propsData: {
documentationPath: 'documentation_path',
newPolicyPath: '/policies/new',
...propsData,
},
data,
store,
provide: {
projectPath: fullPath,
},
apolloProvider: createMockApolloProvider([
[networkPoliciesQuery, requestHandlers.networkPolicies],
]),
stubs: { PolicyDrawer: GlDrawer },
localVue,
});
wrapper = mountFn(
PolicyList,
merge(
{
propsData: {
documentationPath: 'documentation_path',
newPolicyPath: '/policies/new',
},
store,
provide: {
projectPath: fullPath,
},
apolloProvider: createMockApolloProvider([
[networkPoliciesQuery, requestHandlers.networkPolicies],
[scanExecutionPoliciesQuery, requestHandlers.scanExecutionPolicies],
]),
stubs: {
PolicyDrawer: GlDrawer,
},
localVue,
},
wrapperOptions,
),
);
};
const mountShallowWrapper = factory(shallowMountExtended);
const mountWrapper = factory();
const findEnvironmentsPicker = () => wrapper.find({ ref: 'environmentsPicker' });
const findPoliciesTable = () => wrapper.find(GlTable);
const findTableEmptyState = () => wrapper.find({ ref: 'tableEmptyState' });
const findPoliciesTable = () => wrapper.findComponent(GlTable);
const findPolicyDrawer = () => wrapper.findByTestId('policyDrawer');
const findAutodevopsAlert = () => wrapper.findByTestId('autodevopsAlert');
beforeEach(() => {
factory({});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders EnvironmentPicker', () => {
expect(findEnvironmentsPicker().exists()).toBe(true);
});
it('renders the new policy button', () => {
const button = wrapper.find('[data-testid="new-policy"]');
expect(button.exists()).toBe(true);
});
describe('initial state', () => {
beforeEach(() => {
factory({
mountFn: shallowMountExtended,
mountShallowWrapper({
handlers: {
networkPolicies: pendingHandler,
},
});
});
it('renders EnvironmentPicker', () => {
expect(findEnvironmentsPicker().exists()).toBe(true);
});
it('renders the new policy button', () => {
const button = wrapper.find('[data-testid="new-policy"]');
expect(button.exists()).toBe(true);
});
it('renders closed editor drawer', () => {
const editorDrawer = findPolicyDrawer();
expect(editorDrawer.exists()).toBe(true);
expect(editorDrawer.props('open')).toBe(false);
});
it('does not render autodevops alert', () => {
expect(findAutodevopsAlert().exists()).toBe(false);
});
it('fetches policies', () => {
expect(requestHandlers.networkPolicies).toHaveBeenCalledWith({
fullPath,
});
expect(requestHandlers.scanExecutionPolicies).toHaveBeenCalledWith({
fullPath,
});
});
it("sets table's loading state", () => {
expect(findPoliciesTable().attributes('busy')).toBe('true');
it('fetches network policies on environment change', async () => {
store.dispatch.mockReset();
await store.commit('threatMonitoring/SET_CURRENT_ENVIRONMENT_ID', 2);
expect(requestHandlers.networkPolicies).toHaveBeenCalledTimes(2);
expect(requestHandlers.networkPolicies.mock.calls[1][0]).toEqual({
fullPath: 'project/path',
environmentId: environments[0].global_id,
});
});
});
it('fetches policies on environment change', async () => {
store.dispatch.mockReset();
await store.commit('threatMonitoring/SET_CURRENT_ENVIRONMENT_ID', 2);
expect(requestHandlers.networkPolicies).toHaveBeenCalledTimes(2);
expect(requestHandlers.networkPolicies.mock.calls[1][0]).toEqual({
fullPath: 'project/path',
environmentId: environments[0].global_id,
it("sets table's loading state", () => {
expect(findPoliciesTable().attributes('busy')).toBe('true');
});
});
describe('given selected policy is a cilium policy', () => {
describe('given policies have been fetched', () => {
beforeEach(() => {
findPoliciesTable().vm.$emit('row-selected', [mockCiliumPolicy]);
mountShallowWrapper({
stubs: {
GlTable: stubComponent(GlTable, {
template: '<table data-testid="table" />',
props: ['items'],
}),
},
});
});
it('renders the new policy drawer', () => {
expect(findPolicyDrawer().exists()).toBe(true);
it('passes all policies to the table', () => {
expect(cloneDeep(wrapper.findByTestId('table').props('items'))).toEqual([
expect.objectContaining({
name: mockPoliciesResponse[0].name,
}),
expect.objectContaining({
name: 'drop-outbound',
}),
expect.objectContaining({
name: 'allow-inbound-http',
}),
expect.objectContaining({
name: mockScanExecutionPolicy.name,
}),
]);
});
});
it('renders policies table', () => {
expect(findPoliciesTable().element).toMatchSnapshot();
});
describe('with allEnvironments enabled', () => {
beforeEach(() => {
mountWrapper();
wrapper.vm.$store.state.threatMonitoring.allEnvironments = true;
});
......@@ -141,28 +179,9 @@ describe('NetworkPolicyList component', () => {
});
});
it('renders closed editor drawer', () => {
const editorDrawer = findPolicyDrawer();
expect(editorDrawer.exists()).toBe(true);
expect(editorDrawer.props('open')).toBe(false);
});
it('renders opened editor drawer on row selection', () => {
findPoliciesTable().find('td').trigger('click');
return wrapper.vm.$nextTick().then(() => {
const editorDrawer = findPolicyDrawer();
expect(editorDrawer.exists()).toBe(true);
expect(editorDrawer.props('open')).toBe(true);
});
});
it('does not render autodevops alert', () => {
expect(findAutodevopsAlert().exists()).toBe(false);
});
describe('given there is a selected policy', () => {
beforeEach(() => {
mountShallowWrapper();
findPoliciesTable().vm.$emit('row-selected', [mockPoliciesResponse[0]]);
});
......@@ -173,28 +192,14 @@ describe('NetworkPolicyList component', () => {
});
});
describe('given there is a default environment with no data to display', () => {
beforeEach(() => {
factory({
state: {
policies: [],
},
});
});
it('shows the table empty state', () => {
expect(findTableEmptyState().element).toMatchSnapshot();
});
});
describe('given autodevops selected policy', () => {
describe('given an autodevops policy', () => {
beforeEach(() => {
const autoDevOpsPolicy = {
...mockPoliciesResponse[0],
name: 'auto-devops',
fromAutoDevops: true,
};
factory({
mountShallowWrapper({
handlers: {
networkPolicies: networkPolicies([autoDevOpsPolicy]),
},
......
......@@ -38,3 +38,14 @@ export const networkPolicies = (nodes) =>
},
},
});
export const scanExecutionPolicies = (nodes) =>
jest.fn().mockResolvedValue({
data: {
project: {
scanExecutionPolicies: {
nodes,
},
},
},
});
......@@ -68,6 +68,7 @@ actions:
scanner_profile: Scanner Profile
site_profile: Site Profile
`,
enabled: true,
};
export const mockNominalHistory = [
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment