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 {
ProjectListItem,
},
props: {
maxListHeight: {
type: Number,
required: false,
default: 402,
},
projectSearchResults: {
type: Array,
required: true,
......@@ -101,7 +106,7 @@ export default {
<div class="d-flex flex-column">
<gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" />
<gl-infinite-scroll
:max-list-height="402"
:max-list-height="maxListHeight"
:fetched-items="projectSearchResults.length"
:total-items="totalResults"
@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
include SecurityAndCompliancePermissions
before_action :authorize_security_orchestration_policies!
before_action :authorize_update_security_orchestration_policy_project!, only: [:assign]
before_action do
push_frontend_feature_flag(:security_orchestration_policies_configuration, project)
......@@ -16,21 +15,7 @@ module Projects
feature_category :security_orchestration
def show
@assigned_policy_id = project&.security_orchestration_policy_configuration&.security_policy_management_project_id
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)
render :show, locals: { project: project }
end
private
......@@ -38,10 +23,6 @@ module Projects
def check_feature_flag!
render_404 if Feature.disabled?(:security_orchestration_policies_configuration, project)
end
def policy_project_params
params.require(:orchestration).permit(:policy_project_id)
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")
- disable_security_policy_project = !can_update_security_orchestration_policy_project?(project)
%h2.gl-mb-8
= s_("SecurityOrchestration|Create a policy")
= form_with url: assign_project_security_policy_url(@project), as: :policy_project, html: { class: 'gl-w-half' } do |field|
%h4
= 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')
#js-security-policies-list{ data: { assigned_policy_project: assigned_policy_project(project).to_json,
disable_security_policy_project: disable_security_policy_project.to_s,
documentation_path: help_page_path('user/project/clusters/protect/container_network_security/quick_start_guide'),
project_path: project.full_path } }
......@@ -60,9 +60,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :dashboard, only: [:index], controller: :dashboard
resources :vulnerability_report, only: [:index], controller: :vulnerability_report
resource :policy, only: [:show] do
post :assign
end
resource :policy, only: [:show]
resource :configuration, only: [], controller: :configuration do
post :auto_fix, on: :collection
......
......@@ -16,6 +16,7 @@ exports[`dashboard should match the snapshot 1`] = `
titletag="h4"
>
<project-selector-stub
maxlistheight="402"
projectsearchresults=""
selectedprojects=""
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({
export const loadingQuerySpy = jest.fn().mockReturnValue(new Promise(() => {}));
export const apolloFailureResponse = jest.fn().mockRejectedValue();
export const getAlertDetailsQuerySpy = jest.fn().mockResolvedValue({
data: { project: { alertManagementAlerts: { nodes: [mockAlertDetails] } } },
});
......@@ -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
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
......@@ -7,17 +7,20 @@ RSpec.describe "projects/security/policies/show", type: :view do
let(:project) { create(:project) }
before do
assign(:project, project)
stub_feature_flags(security_orchestration_policies_configuration: true)
stub_licensed_features(security_orchestration_policies: true)
sign_in(user)
render
render template: 'projects/security/policies/show', locals: { project: project }
end
it 'renders the default state' do
expect(rendered).to have_selector('h2')
expect(rendered).to have_selector('h4')
expect(response).to have_css('input[id=orchestration_policy_project_id]', visible: false)
expect(rendered).to have_button('Save changes')
it 'renders Vue app root' do
expect(rendered).to have_selector('#js-security-policies-list')
end
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
......@@ -23015,9 +23015,6 @@ msgstr ""
msgid "Only include features new to your current subscription tier."
msgstr ""
msgid "Only owners can update Security Policy Project"
msgstr ""
msgid "Only policy:"
msgstr ""
......@@ -23099,9 +23096,6 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
msgid "Operation completed"
msgstr ""
msgid "Operation failed. Check pod logs for %{pod_name} for more details."
msgstr ""
......@@ -29051,15 +29045,24 @@ msgstr ""
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
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 ""
msgid "SecurityOrchestration|Create a policy"
msgstr ""
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
msgstr ""
msgid "SecurityOrchestration|Security policy project"
msgstr ""
msgid "SecurityOrchestration|Security policy project was linked successfully"
msgstr ""
msgid "SecurityPolicies|+%{count} more"
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