Commit 32174a42 authored by Alexander Turinske's avatar Alexander Turinske

Create policies vue app

- move security policy project selector into vue
- implement project selection mutation
- add default height for project selector
- refactor project selector logic out into a separate compponent
- remove backend assign action
- add tooltip for users without permission to
  change the project
- abstract out data processing to a helper
- clean up instance-procject-selector to be
  more readable
- fix null project
- return null from backend
- add default prop for assignedPolicyProject
- update text
- update tests
parent 0edb65c0
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getProjects(
$search: String!
$after: String = ""
$first: Int!
$searchNamespaces: Boolean = false
$sort: String
$membership: Boolean = true
) {
projects(
search: $search
after: $after
first: $first
membership: $membership
searchNamespaces: $searchNamespaces
sort: $sort
) {
nodes {
id
name
nameWithNamespace
}
pageInfo {
...PageInfo
}
}
}
...@@ -15,6 +15,11 @@ export default { ...@@ -15,6 +15,11 @@ export default {
ProjectListItem, ProjectListItem,
}, },
props: { props: {
maxListHeight: {
type: Number,
required: false,
default: 402,
},
projectSearchResults: { projectSearchResults: {
type: Array, type: Array,
required: true, required: true,
...@@ -101,7 +106,7 @@ export default { ...@@ -101,7 +106,7 @@ export default {
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" /> <gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" />
<gl-infinite-scroll <gl-infinite-scroll
:max-list-height="402" :max-list-height="maxListHeight"
:fetched-items="projectSearchResults.length" :fetched-items="projectSearchResults.length"
:total-items="totalResults" :total-items="totalResults"
@bottomReached="bottomReached" @bottomReached="bottomReached"
......
import initSecurityPoliciesList from 'ee/threat_monitoring/security_policies_list';
initSecurityPoliciesList();
<script>
import produce from 'immer';
import getUsersProjects from '~/graphql_shared/queries/get_users_projects.query.graphql';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
const defaultPageInfo = { endCursor: '', hasNextPage: false };
export default {
MINIMUM_QUERY_LENGTH: 3,
PROJECTS_PER_PAGE: 20,
SEARCH_ERROR: 'SEARCH_ERROR',
QUERY_TOO_SHORT_ERROR: 'QUERY_TOO_SHORT_ERROR',
NO_RESULTS_ERROR: 'NO_RESULTS_ERROR',
apollo: {
projects: {
query: getUsersProjects,
variables() {
return {
search: this.searchQuery,
first: this.$options.PROJECTS_PER_PAGE,
searchNamespaces: true,
sort: 'similarity',
};
},
update(data) {
return data?.projects?.nodes || [];
},
result({ data }) {
const projects = data?.projects || {};
this.pageInfo = projects.pageInfo || defaultPageInfo;
if (projects.nodes?.length === 0) {
this.setErrorType(this.$options.NO_RESULTS_ERROR);
}
},
error() {
this.fetchProjectsError();
},
skip() {
return this.isSearchQueryTooShort;
},
},
},
components: {
ProjectSelector,
},
props: {
selectedProjects: {
type: Array,
required: false,
default: () => [],
},
maxListHeight: {
type: Number,
required: false,
default: 402,
},
projectQuery: {
type: Object,
required: false,
default: () => getUsersProjects,
},
},
data() {
return {
errorType: null,
projects: [],
searchQuery: '',
pageInfo: defaultPageInfo,
};
},
computed: {
isSearchingProjects() {
return this.$apollo.queries.projects.loading;
},
isLoadingFirstResult() {
return this.isSearchingProjects && this.projects.length === 0;
},
isSearchQueryTooShort() {
return this.searchQuery.length < this.$options.MINIMUM_QUERY_LENGTH;
},
},
methods: {
cancelSearch() {
this.projects = [];
this.pageInfo = defaultPageInfo;
this.setErrorType(this.$options.QUERY_TOO_SHORT_ERROR);
},
fetchNextPage() {
if (this.pageInfo.hasNextPage) {
this.$apollo.queries.projects.fetchMore({
variables: { after: this.pageInfo.endCursor },
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
return produce(fetchMoreResult, (draftData) => {
draftData.projects.nodes = [
...previousResult.projects.nodes,
...draftData.projects.nodes,
];
});
},
});
}
},
fetchProjects(query) {
this.searchQuery = query;
if (this.isSearchQueryTooShort) {
this.cancelSearch();
} else {
this.errorType = null;
this.pageInfo = defaultPageInfo;
this.projects = [];
}
},
fetchProjectsError() {
this.projects = [];
this.setErrorType(this.$options.SEARCH_ERROR);
},
isErrorOfType(type) {
return this.errorType === type;
},
setErrorType(errorType) {
this.errorType = errorType;
},
},
};
</script>
<template>
<project-selector
class="gl-w-full"
:max-list-height="maxListHeight"
:project-search-results="projects"
:selected-projects="selectedProjects"
:show-loading-indicator="isLoadingFirstResult"
:show-minimum-search-query-message="isErrorOfType($options.QUERY_TOO_SHORT_ERROR)"
:show-no-results-message="isErrorOfType($options.NO_RESULTS_ERROR)"
:show-search-error-message="isErrorOfType($options.SEARCH_ERROR)"
@searched="fetchProjects"
@projectClicked="$emit('projectClicked', $event)"
@bottomReached="fetchNextPage"
/>
</template>
<script>
import { GlAlert, GlButton, GlDropdown, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import assignSecurityPolicyProject from '../graphql/mutations/assign_security_policy_project.mutation.graphql';
import InstanceProjectSelector from './instance_project_selector.vue';
export default {
PROJECT_SELECTOR_HEIGHT: 204,
i18n: {
assignError: s__(
'SecurityOrchestration|An error occurred assigning your security policy project',
),
assignSuccess: s__('SecurityOrchestration|Security policy project was linked successfully'),
disabledButtonTooltip: s__(
'SecurityOrchestration|Only owners can update Security Policy Project',
),
securityProject: s__(
'SecurityOrchestration|A security policy project can enforce policies for a given project, group, or instance. With a security policy project, you can specify security policies that are important to you and enforce them with every commit. %{linkStart}More information.%{linkEnd}',
),
},
components: {
GlAlert,
GlButton,
GlDropdown,
GlSprintf,
InstanceProjectSelector,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['disableSecurityPolicyProject', 'documentationPath', 'projectPath'],
props: {
assignedPolicyProject: {
type: Object,
required: false,
default: () => {
return { id: '', name: '' };
},
},
},
data() {
return {
currentProjectId: this.assignedPolicyProject.id,
selectedProject: this.assignedPolicyProject,
isAssigningProject: false,
showAssignError: false,
showAssignSuccess: false,
};
},
computed: {
hasSelectedNewProject() {
return this.currentProjectId !== this.selectedProject.id;
},
},
methods: {
dismissAlert(type) {
this[type] = false;
},
async saveChanges() {
this.isAssigningProject = true;
this.showAssignError = false;
this.showAssignSuccess = false;
const { id } = this.selectedProject;
try {
const { data } = await this.$apollo.mutate({
mutation: assignSecurityPolicyProject,
variables: {
input: {
projectPath: this.projectPath,
securityPolicyProjectId: id,
},
},
});
if (data?.securityPolicyProjectAssign?.errors?.length) {
this.showAssignError = true;
} else {
this.showAssignSuccess = true;
this.currentProjectId = id;
}
} catch {
this.showAssignError = true;
} finally {
this.isAssigningProject = false;
}
},
setSelectedProject(data) {
this.selectedProject = data;
this.$refs.dropdown.hide(true);
},
},
};
</script>
<template>
<section>
<gl-alert
v-if="showAssignError"
class="gl-mt-3"
data-testid="policy-project-assign-error"
variant="danger"
:dismissible="true"
@dismiss="dismissAlert('showAssignError')"
>
{{ $options.i18n.assignError }}
</gl-alert>
<gl-alert
v-else-if="showAssignSuccess"
class="gl-mt-3"
data-testid="policy-project-assign-success"
variant="success"
:dismissible="true"
@dismiss="dismissAlert('showAssignSuccess')"
>
{{ $options.i18n.assignSuccess }}
</gl-alert>
<h2 class="gl-mb-8">
{{ s__('SecurityOrchestration|Create a policy') }}
</h2>
<div class="gl-w-half">
<h4>
{{ s__('SecurityOrchestration|Security policy project') }}
</h4>
<gl-dropdown
ref="dropdown"
class="gl-w-full gl-pb-5 security-policy-dropdown"
menu-class="gl-w-full! gl-max-w-full!"
:disabled="disableSecurityPolicyProject"
:text="selectedProject.name || ''"
>
<instance-project-selector
class="gl-w-full"
:max-list-height="$options.PROJECT_SELECTOR_HEIGHT"
:selected-projects="[selectedProject]"
@projectClicked="setSelectedProject"
/>
</gl-dropdown>
<div class="gl-pb-5">
<gl-sprintf :message="$options.i18n.securityProject">
<template #link="{ content }">
<gl-button class="gl-pb-1!" variant="link" :href="documentationPath" target="_blank">
{{ content }}
</gl-button>
</template>
</gl-sprintf>
</div>
<span
v-gl-tooltip="{
disabled: !disableSecurityPolicyProject,
title: $options.i18n.disabledButtonTooltip,
placement: 'bottom',
}"
data-testid="disabled-button-tooltip"
>
<gl-button
data-testid="save-policy-project"
variant="confirm"
:disabled="disableSecurityPolicyProject || !hasSelectedNewProject"
:loading="isAssigningProject"
@click="saveChanges"
>
{{ __('Save changes') }}
</gl-button>
</span>
</div>
</section>
</template>
mutation assignSecurityPolicyProject($input: SecurityPolicyProjectAssignInput!) {
securityPolicyProjectAssign(input: $input) {
errors
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import SecurityPolicyProjectSelector from './components/security_policy_project_selector.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export default () => {
const el = document.querySelector('#js-security-policies-list');
const {
assignedPolicyProject,
disableSecurityPolicyProject,
documentationPath,
projectPath,
} = el.dataset;
const policyProject = JSON.parse(assignedPolicyProject);
const props = policyProject ? { assignedPolicyProject: policyProject } : {};
return new Vue({
apolloProvider,
el,
provide: {
disableSecurityPolicyProject: parseBoolean(disableSecurityPolicyProject),
documentationPath,
projectPath,
},
render(createElement) {
return createElement(SecurityPolicyProjectSelector, {
props,
});
},
});
};
...@@ -6,7 +6,6 @@ module Projects ...@@ -6,7 +6,6 @@ module Projects
include SecurityAndCompliancePermissions include SecurityAndCompliancePermissions
before_action :authorize_security_orchestration_policies! before_action :authorize_security_orchestration_policies!
before_action :authorize_update_security_orchestration_policy_project!, only: [:assign]
before_action do before_action do
push_frontend_feature_flag(:security_orchestration_policies_configuration, project) push_frontend_feature_flag(:security_orchestration_policies_configuration, project)
...@@ -16,21 +15,7 @@ module Projects ...@@ -16,21 +15,7 @@ module Projects
feature_category :security_orchestration feature_category :security_orchestration
def show def show
@assigned_policy_id = project&.security_orchestration_policy_configuration&.security_policy_management_project_id render :show, locals: { project: project }
render :show
end
def assign
result = ::Security::Orchestration::AssignService.new(project, nil, policy_project_id: policy_project_params[:policy_project_id]).execute
if result.success?
flash[:notice] = _('Operation completed')
else
flash[:alert] = result.message
end
redirect_to project_security_policy_url(project)
end end
private private
...@@ -38,10 +23,6 @@ module Projects ...@@ -38,10 +23,6 @@ module Projects
def check_feature_flag! def check_feature_flag!
render_404 if Feature.disabled?(:security_orchestration_policies_configuration, project) render_404 if Feature.disabled?(:security_orchestration_policies_configuration, project)
end end
def policy_project_params
params.require(:orchestration).permit(:policy_project_id)
end
end end
end end
end end
# frozen_string_literal: true
module Projects::Security::PoliciesHelper
def assigned_policy_project(project)
return unless project&.security_orchestration_policy_configuration
orchestration_policy_configuration = project.security_orchestration_policy_configuration
security_policy_management_project = orchestration_policy_configuration.security_policy_management_project
{ id: security_policy_management_project.to_global_id.to_s, name: security_policy_management_project.name }
end
end
- breadcrumb_title _("Scan Policies") - breadcrumb_title _("Scan Policies")
- disable_security_policy_project = !can_update_security_orchestration_policy_project?(project)
%h2.gl-mb-8 #js-security-policies-list{ data: { assigned_policy_project: assigned_policy_project(project).to_json,
= s_("SecurityOrchestration|Create a policy") disable_security_policy_project: disable_security_policy_project.to_s,
= form_with url: assign_project_security_policy_url(@project), as: :policy_project, html: { class: 'gl-w-half' } do |field| documentation_path: help_page_path('user/project/clusters/protect/container_network_security/quick_start_guide'),
%h4 project_path: project.full_path } }
= s_('SecurityOrchestration|Security policy project')
%p
= project_select_tag('orchestration[policy_project_id]', class: 'hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
placeholder: _('Select project'), idAttribute: 'id', disabled: !can_update_security_orchestration_policy_project?(@project), data: { order_by: 'last_activity_at', idattribute: 'id', simple_filter: true, allow_clear: true, include_groups: false, include_projects_in_subgroups: true, user_id: current_user.id }, value: @assigned_policy_id)
.text-muted
= html_escape(s_('SecurityOrchestration|A security policy project can be used enforce policies for a given project, group, or instance. It allows you to specify security policies that are important to you and enforce them with every commit.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('More information'), help_page_path('user/project/clusters/protect/container_network_security/quick_start_guide'), target: '_blank'
- if can_update_security_orchestration_policy_project?(@project)
= field.submit _('Save changes'), class: 'btn gl-button btn-success'
- else
= field.submit _('Save changes'), class: 'btn gl-button btn-success has-tooltip', disabled: true, title: _('Only owners can update Security Policy Project')
...@@ -60,9 +60,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -60,9 +60,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :dashboard, only: [:index], controller: :dashboard resources :dashboard, only: [:index], controller: :dashboard
resources :vulnerability_report, only: [:index], controller: :vulnerability_report resources :vulnerability_report, only: [:index], controller: :vulnerability_report
resource :policy, only: [:show] do resource :policy, only: [:show]
post :assign
end
resource :configuration, only: [], controller: :configuration do resource :configuration, only: [], controller: :configuration do
post :auto_fix, on: :collection post :auto_fix, on: :collection
......
...@@ -16,6 +16,7 @@ exports[`dashboard should match the snapshot 1`] = ` ...@@ -16,6 +16,7 @@ exports[`dashboard should match the snapshot 1`] = `
titletag="h4" titletag="h4"
> >
<project-selector-stub <project-selector-stub
maxlistheight="402"
projectsearchresults="" projectsearchresults=""
selectedprojects="" selectedprojects=""
totalresults="0" totalresults="0"
......
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import InstanceProjectSelector from 'ee/threat_monitoring/components/instance_project_selector.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getUsersProjects from '~/graphql_shared/queries/get_users_projects.query.graphql';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
const localVue = createLocalVue();
let querySpy;
const defaultProjectSelectorProps = {
maxListHeight: 402,
projectSearchResults: [],
selectedProjects: [],
showLoadingIndicator: false,
showMinimumSearchQueryMessage: false,
showNoResultsMessage: false,
showSearchErrorMessage: false,
totalResults: 0,
};
const defaultQueryVariables = {
after: '',
first: 20,
membership: true,
search: 'abc',
searchNamespaces: true,
sort: 'similarity',
};
const defaultPageInfo = {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
};
const querySuccess = {
data: {
projects: {
nodes: [
{
id: 'gid://gitlab/Project/5000162',
name: 'Pages Test Again',
nameWithNamespace: 'mixed-vulnerabilities-01 / Pages Test Again',
},
],
pageInfo: { hasNextPage: true, hasPreviousPage: false, startCursor: 'a', endCursor: 'z' },
},
},
};
const queryError = {
errors: [
{
message: 'test',
locations: [[{ line: 1, column: 58 }]],
extensions: {
value: null,
problems: [{ path: [], explanation: 'Expected value to not be null' }],
},
},
],
};
const mockGetUsersProjects = {
empty: { data: { projects: { nodes: [], pageInfo: defaultPageInfo } } },
error: queryError,
success: querySuccess,
};
const createMockApolloProvider = (queryResolver) => {
localVue.use(VueApollo);
return createMockApollo([[getUsersProjects, queryResolver]]);
};
describe('InstanceProjectSelector Component', () => {
let wrapper;
const findProjectSelector = () => wrapper.findComponent(ProjectSelector);
const createWrapper = ({ queryResolver, propsData = {} } = {}) => {
wrapper = shallowMountExtended(InstanceProjectSelector, {
localVue,
apolloProvider: createMockApolloProvider(queryResolver),
propsData: {
...propsData,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
querySpy = jest.fn().mockResolvedValue(mockGetUsersProjects.success);
createWrapper({ queryResolver: querySpy });
});
it('renders the project selector', () => {
expect(findProjectSelector().props()).toStrictEqual(defaultProjectSelectorProps);
});
it('does not query when the search query is less than three characters', async () => {
findProjectSelector().vm.$emit('searched', '');
await waitForPromises();
expect(querySpy).not.toHaveBeenCalled();
});
it('does query when the search query is more than three characters', async () => {
findProjectSelector().vm.$emit('searched', 'abc');
await waitForPromises();
expect(querySpy).toHaveBeenCalledTimes(1);
expect(querySpy).toHaveBeenCalledWith(defaultQueryVariables);
});
it('does query when the bottom is reached', async () => {
expect(querySpy).toHaveBeenCalledTimes(0);
findProjectSelector().vm.$emit('searched', 'abc');
await waitForPromises();
expect(querySpy).toHaveBeenCalledTimes(1);
findProjectSelector().vm.$emit('bottomReached');
await waitForPromises();
expect(querySpy).toHaveBeenCalledTimes(2);
expect(querySpy).toHaveBeenCalledWith({
...defaultQueryVariables,
after: 'z',
});
});
it('emits on "projectClicked"', () => {
const project = { id: 0, name: 'test' };
findProjectSelector().vm.$emit('projectClicked', project);
expect(wrapper.emitted('projectClicked')).toStrictEqual([[project]]);
});
});
describe('other states', () => {
it('notifies project selector of search error', async () => {
querySpy = jest.fn().mockResolvedValue(mockGetUsersProjects.error);
createWrapper({ queryResolver: querySpy });
await wrapper.vm.$nextTick();
findProjectSelector().vm.$emit('searched', 'abc');
await waitForPromises();
expect(findProjectSelector().props()).toStrictEqual({
...defaultProjectSelectorProps,
showSearchErrorMessage: true,
});
});
it('notifies project selector of no results', async () => {
querySpy = jest.fn().mockResolvedValue(mockGetUsersProjects.empty);
createWrapper({ queryResolver: querySpy });
await wrapper.vm.$nextTick();
findProjectSelector().vm.$emit('searched', 'abc');
await waitForPromises();
expect(findProjectSelector().props()).toStrictEqual({
...defaultProjectSelectorProps,
showNoResultsMessage: true,
});
});
});
});
import { GlDropdown } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import InstanceProjectSelector from 'ee/threat_monitoring/components/instance_project_selector.vue';
import SecurityPolicyProjectSelector from 'ee/threat_monitoring/components/security_policy_project_selector.vue';
import assignSecurityPolicyProject from 'ee/threat_monitoring/graphql/mutations/assign_security_policy_project.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
apolloFailureResponse,
mockAssignSecurityPolicyProjectResponses,
} from '../mocks/mock_apollo';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('SecurityPolicyProjectSelector Component', () => {
let wrapper;
const findSaveButton = () => wrapper.findByTestId('save-policy-project');
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findErrorAlert = () => wrapper.findByTestId('policy-project-assign-error');
const findInstanceProjectSelector = () => wrapper.findComponent(InstanceProjectSelector);
const findSuccessAlert = () => wrapper.findByTestId('policy-project-assign-success');
const findTooltip = () => wrapper.findByTestId('disabled-button-tooltip');
const selectProject = async () => {
findInstanceProjectSelector().vm.$emit('projectClicked', {
id: 'gid://gitlab/Project/1',
name: 'Test 1',
});
await wrapper.vm.$nextTick();
findSaveButton().vm.$emit('click');
await waitForPromises();
};
const createWrapper = ({
mount = shallowMountExtended,
mutationResult = mockAssignSecurityPolicyProjectResponses.success,
propsData = {},
provide = {},
} = {}) => {
wrapper = mount(SecurityPolicyProjectSelector, {
localVue,
apolloProvider: createMockApollo([[assignSecurityPolicyProject, mutationResult]]),
directives: {
GlTooltip: createMockDirective(),
},
propsData,
provide: {
disableSecurityPolicyProject: false,
documentationPath: 'test/path/index.md',
projectPath: 'path/to/project',
...provide,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
createWrapper();
});
it.each`
findComponent | state | title
${findDropdown} | ${true} | ${'does display the dropdown'}
${findInstanceProjectSelector} | ${true} | ${'does display the project selector'}
${findErrorAlert} | ${false} | ${'does not display the error alert'}
${findSuccessAlert} | ${false} | ${'does not display the success alert'}
`('$title', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('renders the "Save Changes" button', () => {
const button = findSaveButton();
expect(button.exists()).toBe(true);
expect(button.attributes('disabled')).toBe('true');
});
it('does not display a tooltip', () => {
const tooltip = getBinding(findTooltip().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(true);
});
});
describe('project selection', () => {
it('enables the "Save Changes" button if a new project is selected', async () => {
createWrapper({
mount: mountExtended,
propsData: { assignedPolicyProject: { id: 'gid://gitlab/Project/0', name: 'Test 0' } },
});
const button = findSaveButton();
expect(button.attributes('disabled')).toBe('disabled');
findInstanceProjectSelector().vm.$emit('projectClicked', {
id: 'gid://gitlab/Project/1',
name: 'Test 1',
});
await wrapper.vm.$nextTick();
expect(button.attributes('disabled')).toBe(undefined);
});
it('displays an alert if the security policy project selection succeeds', async () => {
createWrapper({ mount: mountExtended });
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(false);
await selectProject();
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(true);
});
it('shows an alert if the security policy project selection fails', async () => {
createWrapper({
mount: mountExtended,
mutationResult: mockAssignSecurityPolicyProjectResponses.failure,
});
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(false);
await selectProject();
expect(findErrorAlert().exists()).toBe(true);
expect(findSuccessAlert().exists()).toBe(false);
});
it('shows an alert if GraphQL fails', async () => {
createWrapper({ mount: mountExtended, mutationResult: apolloFailureResponse });
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(false);
await selectProject();
expect(findErrorAlert().exists()).toBe(true);
expect(findSuccessAlert().exists()).toBe(false);
});
});
describe('disabled', () => {
beforeEach(() => {
createWrapper({ provide: { disableSecurityPolicyProject: true } });
});
it('disables the dropdown', () => {
expect(findDropdown().attributes('disabled')).toBe('true');
});
it('displays a tooltip', () => {
const tooltip = getBinding(findTooltip().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(false);
});
});
});
...@@ -17,6 +17,8 @@ export const emptyGetAlertsQuerySpy = jest.fn().mockResolvedValue({ ...@@ -17,6 +17,8 @@ export const emptyGetAlertsQuerySpy = jest.fn().mockResolvedValue({
export const loadingQuerySpy = jest.fn().mockReturnValue(new Promise(() => {})); export const loadingQuerySpy = jest.fn().mockReturnValue(new Promise(() => {}));
export const apolloFailureResponse = jest.fn().mockRejectedValue();
export const getAlertDetailsQuerySpy = jest.fn().mockResolvedValue({ export const getAlertDetailsQuerySpy = jest.fn().mockResolvedValue({
data: { project: { alertManagementAlerts: { nodes: [mockAlertDetails] } } }, data: { project: { alertManagementAlerts: { nodes: [mockAlertDetails] } } },
}); });
...@@ -49,3 +51,10 @@ export const scanExecutionPolicies = (nodes) => ...@@ -49,3 +51,10 @@ export const scanExecutionPolicies = (nodes) =>
}, },
}, },
}); });
export const mockAssignSecurityPolicyProjectResponses = {
success: jest.fn().mockResolvedValue({ data: { securityPolicyProjectAssign: { errors: [] } } }),
failure: jest
.fn()
.mockResolvedValue({ data: { securityPolicyProjectAssign: { errors: ['mutation failed'] } } }),
};
...@@ -37,41 +37,4 @@ RSpec.describe Projects::Security::PoliciesController, type: :request do ...@@ -37,41 +37,4 @@ RSpec.describe Projects::Security::PoliciesController, type: :request do
end end
end end
end end
context 'assign action' do
let_it_be(:policy_project, reload: true) { create(:project) }
before do
stub_feature_flags(security_orchestration_policies_configuration: true)
stub_licensed_features(security_orchestration_policies: true)
end
context 'when user is not an owner of the project' do
it 'returns error message' do
post assign_project_security_policy_url(project), params: { orchestration: { policy_project_id: policy_project.id } }
expect(response).to have_gitlab_http_status(:not_found)
expect(response).not_to render_template('new')
end
end
context 'when user is an owner of the project' do
before do
login_as(owner)
end
it 'assigns policy project to project' do
post assign_project_security_policy_url(project), params: { orchestration: { policy_project_id: policy_project.id } }
expect(response).to redirect_to(project_security_policy_url(project))
expect(project.security_orchestration_policy_configuration.security_policy_management_project_id).to eq(policy_project.id)
end
it 'returns error message for invalid input' do
post assign_project_security_policy_url(project), params: { orchestration: { policy_project_id: nil } }
expect(flash[:alert]).to eq 'Policy project doesn\'t exist'
end
end
end
end end
...@@ -7,17 +7,20 @@ RSpec.describe "projects/security/policies/show", type: :view do ...@@ -7,17 +7,20 @@ RSpec.describe "projects/security/policies/show", type: :view do
let(:project) { create(:project) } let(:project) { create(:project) }
before do before do
assign(:project, project)
stub_feature_flags(security_orchestration_policies_configuration: true) stub_feature_flags(security_orchestration_policies_configuration: true)
stub_licensed_features(security_orchestration_policies: true)
sign_in(user) sign_in(user)
render render template: 'projects/security/policies/show', locals: { project: project }
end end
it 'renders the default state' do it 'renders Vue app root' do
expect(rendered).to have_selector('h2') expect(rendered).to have_selector('#js-security-policies-list')
expect(rendered).to have_selector('h4') end
expect(response).to have_css('input[id=orchestration_policy_project_id]', visible: false)
expect(rendered).to have_button('Save changes') it "passes project's full path" do
expect(rendered).to include project.path_with_namespace
end
it 'passes documentation URL' do
expect(rendered).to include '/help/user/project/clusters/protect/container_network_security/quick_start_guide'
end end
end end
...@@ -23015,9 +23015,6 @@ msgstr "" ...@@ -23015,9 +23015,6 @@ msgstr ""
msgid "Only include features new to your current subscription tier." msgid "Only include features new to your current subscription tier."
msgstr "" msgstr ""
msgid "Only owners can update Security Policy Project"
msgstr ""
msgid "Only policy:" msgid "Only policy:"
msgstr "" msgstr ""
...@@ -23099,9 +23096,6 @@ msgstr "" ...@@ -23099,9 +23096,6 @@ msgstr ""
msgid "Opens in a new window" msgid "Opens in a new window"
msgstr "" msgstr ""
msgid "Operation completed"
msgstr ""
msgid "Operation failed. Check pod logs for %{pod_name} for more details." msgid "Operation failed. Check pod logs for %{pod_name} for more details."
msgstr "" msgstr ""
...@@ -29051,15 +29045,24 @@ msgstr "" ...@@ -29051,15 +29045,24 @@ msgstr ""
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}." msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
msgstr "" msgstr ""
msgid "SecurityOrchestration|A security policy project can be used enforce policies for a given project, group, or instance. It allows you to specify security policies that are important to you and enforce them with every commit." msgid "SecurityOrchestration|A security policy project can enforce policies for a given project, group, or instance. With a security policy project, you can specify security policies that are important to you and enforce them with every commit. %{linkStart}More information.%{linkEnd}"
msgstr ""
msgid "SecurityOrchestration|An error occurred assigning your security policy project"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Create a policy" msgid "SecurityOrchestration|Create a policy"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
msgstr ""
msgid "SecurityOrchestration|Security policy project" msgid "SecurityOrchestration|Security policy project"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Security policy project was linked successfully"
msgstr ""
msgid "SecurityPolicies|+%{count} more" msgid "SecurityPolicies|+%{count} more"
msgstr "" msgstr ""
......
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