Commit c67b571a authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '336624-restrict-permissions-to-create-an-incident-to-reporter-and-up' into 'master'

Restrict permissions to create an incident to Reporter and up

See merge request gitlab-org/gitlab!69350
parents edd23d67 280ff64e
......@@ -125,6 +125,7 @@ export default {
'authorUsernameQuery',
'assigneeUsernameQuery',
'slaFeatureAvailable',
'canCreateIncident',
],
apollo: {
incidents: {
......@@ -230,13 +231,16 @@ export default {
},
emptyStateData() {
const {
emptyState: { title, emptyClosedTabTitle, description },
emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
createIncidentBtnLabel,
} = this.$options.i18n;
if (this.activeClosedTabHasNoIncidents) {
return { title: emptyClosedTabTitle };
}
if (!this.canCreateIncident) {
return { title, description: cannotCreateIncidentDescription };
}
return {
title,
description,
......@@ -244,6 +248,9 @@ export default {
btnText: createIncidentBtnLabel,
};
},
isHeaderButtonVisible() {
return this.canCreateIncident && (!this.isEmpty || this.activeClosedTabHasNoIncidents);
},
},
methods: {
hasAssignees(assignees) {
......@@ -311,7 +318,7 @@ export default {
>
<template #header-actions>
<gl-button
v-if="!isEmpty || activeClosedTabHasNoIncidents"
v-if="isHeaderButtonVisible"
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
......
......@@ -11,7 +11,10 @@ export const I18N = {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
description: s__(
'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.',
'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list. You can also create a new incident using the button below.',
),
cannotCreateIncidentDescription: s__(
'IncidentManagement|All alerts promoted to incidents are automatically displayed within the list.',
),
},
};
......
......@@ -21,6 +21,7 @@ export default () => {
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable,
canCreateIncident,
} = domEl.dataset;
const apolloProvider = new VueApollo({
......@@ -44,6 +45,7 @@ export default () => {
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
canCreateIncident: parseBoolean(canCreateIncident),
},
apolloProvider,
render(createElement) {
......
......@@ -2,7 +2,7 @@
import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { __ } from '~/locale';
import { IssuableTypes } from '../../constants';
import { IssuableTypes, IncidentType } from '../../constants';
import getIssueStateQuery from '../../queries/get_issue_state.query.graphql';
import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql';
......@@ -19,6 +19,14 @@ export default {
GlDropdown,
GlDropdownItem,
},
inject: {
canCreateIncident: {
default: false,
},
issueType: {
default: 'issue',
},
},
data() {
return {
issueState: {},
......@@ -36,6 +44,9 @@ export default {
} = this;
return capitalize(issueType);
},
shouldShowIncident() {
return this.issueType === IncidentType || this.canCreateIncident;
},
},
methods: {
updateIssueType(issueType) {
......@@ -47,6 +58,9 @@ export default {
},
});
},
isShown(type) {
return type.value !== IncidentType || this.shouldShowIncident;
},
},
};
</script>
......@@ -68,6 +82,7 @@ export default {
>
<gl-dropdown-item
v-for="type in $options.IssuableTypes"
v-show="isShown(type)"
:key="type.value"
:is-checked="issueState.issueType === type.value"
is-check-item
......
......@@ -2,25 +2,31 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue';
import { issueState } from './constants';
import { issueState, IncidentType } from './constants';
import apolloProvider from './graphql';
import getIssueStateQuery from './queries/get_issue_state.query.graphql';
import HeaderActions from './components/header_actions.vue';
export default function initIssuableApp(issuableData = {}) {
const bootstrapApollo = (state = {}) => {
return apolloProvider.clients.defaultClient.cache.writeQuery({
query: getIssueStateQuery,
data: {
issueState: state,
},
});
};
export function initIncidentApp(issuableData = {}) {
const el = document.getElementById('js-issuable-app');
if (!el) {
return undefined;
}
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getIssueStateQuery,
data: {
issueState: { ...issueState, issueType: el.dataset.issueType },
},
});
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
const {
canCreateIncident,
canUpdate,
iid,
projectNamespace,
......@@ -39,6 +45,8 @@ export default function initIssuableApp(issuableData = {}) {
issuableApp,
},
provide: {
issueType: IncidentType,
canCreateIncident,
canUpdate,
fullPath,
iid,
......@@ -57,3 +65,35 @@ export default function initIssuableApp(issuableData = {}) {
},
});
}
export function initIncidentHeaderActions(store) {
const el = document.querySelector('.js-issue-header-actions');
if (!el) {
return undefined;
}
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
return new Vue({
el,
apolloProvider,
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIncident),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
issueType: el.dataset.issueType,
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
projectId: el.dataset.projectId,
reportAbusePath: el.dataset.reportAbusePath,
submitAsSpamPath: el.dataset.submitAsSpamPath,
},
render: (createElement) => createElement(HeaderActions),
});
}
......@@ -25,17 +25,22 @@ export function initIssuableApp(issuableData, store) {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
const { canCreateIncident, ...issuableProps } = issuableData;
return new Vue({
el,
apolloProvider,
store,
provide: {
canCreateIncident,
},
computed: {
...mapGetters(['getNoteableData']),
},
render(createElement) {
return createElement(IssuableApp, {
props: {
...issuableData,
...issuableProps,
isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
......
......@@ -3,7 +3,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue';
import initIncidentApp from '~/issue_show/incident';
import { initIncidentApp, initIncidentHeaderActions } from '~/issue_show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
import initNotesApp from '~/notes';
......@@ -22,16 +22,18 @@ export default function initShowIssue() {
switch (issueType) {
case IssuableType.Incident:
initIncidentApp(issuableData);
initIncidentHeaderActions(store);
break;
case IssuableType.Issue:
initIssuableApp(issuableData, store);
initIssueHeaderActions(store);
break;
default:
initIssueHeaderActions(store);
break;
}
initIssuableHeaderWarning(store);
initIssueHeaderActions(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
......
......@@ -257,7 +257,8 @@ module IssuablesHelper
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier, # rubocop:disable CodeReuse/ActiveRecord
iid: issuable.iid.to_s,
isHidden: issue_hidden?(issuable)
isHidden: issue_hidden?(issuable),
canCreateIncident: create_issue_type_allowed?(issuable.project, :incident)
}
end
......
......@@ -192,6 +192,7 @@ module IssuesHelper
{
can_create_issue: show_new_issue_link?(project).to_s,
can_create_incident: create_issue_type_allowed?(project, :incident).to_s,
can_reopen_issue: can?(current_user, :reopen_issue, issuable).to_s,
can_report_spam: issuable.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issuable).to_s,
......
......@@ -11,7 +11,8 @@ module Projects::IncidentsHelper
'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg'),
'text-query': params[:search],
'author-username-query': params[:author_username],
'assignee-username-query': params[:assignee_username]
'assignee-username-query': params[:assignee_username],
'can-create-incident': create_issue_type_allowed?(project, :incident).to_s
}
end
end
......
......@@ -248,7 +248,7 @@ class ProjectPolicy < BasePolicy
enable :read_insights
end
rule { can?(:guest_access) & can?(:create_issue) }.enable :create_incident
rule { can?(:reporter_access) & can?(:create_issue) }.enable :create_incident
# These abilities are not allowed to admins that are not members of the project,
# that's why they are defined separately.
......
......@@ -19,20 +19,22 @@ You can create an incident manually or automatically.
### Create incidents manually
If you have at least Guest [permissions](../../user/permissions.md), to create an
Incident, you have two options to do this manually.
> [Permission changed](https://gitlab.com/gitlab-org/gitlab/-/issues/336624) from Guest to Reporter in GitLab 14.5.
**From the Incidents List:**
If you have at least Reporter [permissions](../../user/permissions.md),
you can create an incident manually from the Incidents List or the Issues List.
To create an incident from the Incidents List:
> [Moved](https://gitlab.com/gitlab-org/monitor/monitor/-/issues/24) to GitLab Free in 13.3.
- Navigate to **Monitor > Incidents** and click **Create Incident**.
- Create a new issue using the `incident` template available when creating it.
- Create a new issue and assign the `incident` label to it.
1. Navigate to **Monitor > Incidents** and click **Create Incident**.
1. Create a new issue using the `incident` template available when creating it.
1. Create a new issue and assign the `incident` label to it.
![Incident List Create](img/incident_list_create_v13_3.png)
**From the Issues List:**
To create an incident from the Issues List:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230857) in GitLab 13.4.
......
......@@ -84,7 +84,7 @@ The following table lists project permissions available for each role:
| [Incident Management](../operations/incident_management/index.md):<br>View [alerts](../operations/incident_management/alerts.md) | | ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>Assign an alert | ✓| ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>View [incident](../operations/incident_management/incidents.md) | ✓| ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>Create [incident](../operations/incident_management/incidents.md) | | ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>Create [incident](../operations/incident_management/incidents.md) | (*17*) | ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>View [on-call schedules](../operations/incident_management/oncall_schedules.md) | | ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>Participate in on-call rotation | ✓| ✓ | ✓ | ✓ | ✓ |
| [Incident Management](../operations/incident_management/index.md):<br>View [escalation policies](../operations/incident_management/escalation_policies.md) | | ✓ | ✓ | ✓ | ✓ |
......@@ -226,6 +226,7 @@ The following table lists project permissions available for each role:
1. Attached design files are moved together with the issue even if the user doesn't have the
Developer role.
1. Guest users can set metadata (for example, labels, assignees, or milestones) when creating an issue.
1. In GitLab 14.5 or later, Guests are not allowed to [create incidents](../operations/incident_management/incidents.md#incident-creation).
## Project features permissions
......
......@@ -15,6 +15,7 @@ const defaultProvide = {
authorUsernameQuery: '',
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
canCreateIncident: true,
};
describe('Incidents Service Level Agreement', () => {
......
......@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::IncidentsHelper do
include Gitlab::Routing.url_helpers
let_it_be_with_refind(:project) { create(:project) }
let(:project) { build_stubbed(:project) }
let(:user) { build_stubbed(:user) }
let(:project_path) { project.full_path }
let(:new_issue_path) { new_project_issue_path(project) }
let(:issue_path) { project_issues_path(project) }
......@@ -18,6 +18,13 @@ RSpec.describe Projects::IncidentsHelper do
}
end
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?)
.with(user, :create_incident, project)
.and_return(true)
end
describe '#incidents_data' do
let(:expected_incidents_data) do
{
......@@ -31,7 +38,8 @@ RSpec.describe Projects::IncidentsHelper do
'sla-feature-available' => 'false',
'text-query': 'search text',
'author-username-query': 'root',
'assignee-username-query': 'max.power'
'assignee-username-query': 'max.power',
'can-create-incident': 'true'
}
end
......
......@@ -18101,7 +18101,10 @@ msgstr ""
msgid "IncidentManagement|All"
msgstr ""
msgid "IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below."
msgid "IncidentManagement|All alerts promoted to incidents are automatically displayed within the list."
msgstr ""
msgid "IncidentManagement|All alerts promoted to incidents are automatically displayed within the list. You can also create a new incident using the button below."
msgstr ""
msgid "IncidentManagement|Assignees"
......
......@@ -4,52 +4,49 @@ require 'spec_helper'
RSpec.describe 'Incident Management index', :js do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
before_all do
project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest)
end
shared_examples 'create incident form' do
it 'shows the create new issue button' do
expect(page).to have_selector('.create-incident-button')
end
before do
sign_in(user)
it 'when clicked shows the create issue page with the Incident type pre-selected' do
find('.create-incident-button').click
wait_for_all_requests
visit project_incidents_path(project)
wait_for_all_requests
end
expect(page).to have_selector('.dropdown-menu-toggle')
expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap')
describe 'incident list is visited' do
context 'by reporter' do
let(:user) { reporter }
page.within('.js-issuable-type-filter-dropdown-wrap') do
expect(page).to have_content('Incident')
it 'shows the create new incident button' do
expect(page).to have_selector('.create-incident-button')
end
end
end
context 'when a developer displays the incident list' do
before do
sign_in(developer)
it 'when clicked shows the create issue page with the Incident type pre-selected' do
find('.create-incident-button').click
wait_for_all_requests
visit project_incidents_path(project)
wait_for_all_requests
end
expect(page).to have_selector('.dropdown-menu-toggle')
expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap')
it_behaves_like 'create incident form'
page.within('.js-issuable-type-filter-dropdown-wrap') do
expect(page).to have_content('Incident')
end
end
end
end
context 'when a guest displays the incident list' do
before do
sign_in(guest)
context 'by guest' do
let(:user) { guest }
visit project_incidents_path(project)
wait_for_all_requests
it 'does not show new incident button' do
expect(page).not_to have_selector('.create-incident-button')
end
it_behaves_like 'create incident form'
end
end
......@@ -22,12 +22,30 @@ RSpec.describe "User views incident" do
it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
it 'shows the merge request and incident actions', :js, :aggregate_failures do
click_button 'Incident actions'
describe 'user actions' do
it 'shows the merge request and incident actions', :js, :aggregate_failures do
click_button 'Incident actions'
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident')
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident')
end
context 'when user is a guest' do
before do
project.add_guest(user)
login_as(user)
visit(project_issues_incident_path(project, incident))
end
it 'does not show the incident action', :js, :aggregate_failures do
click_button 'Incident actions'
expect(page).not_to have_link('New incident')
end
end
end
context 'when the project is archived' do
......
......@@ -248,15 +248,21 @@ RSpec.describe 'New/edit issue', :js do
end
end
shared_examples 'type option is missing' do |label:, identifier:|
it "does not show #{identifier} option", :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).not_to have_selector(%([data-testid="issue-type-#{identifier}-icon"]))
expect(page).not_to have_content(label)
end
end
end
before do
page.within('.issue-form') do
click_button 'Issue'
end
end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
context 'when user is guest' do
let_it_be(:guest) { create(:user) }
......@@ -266,6 +272,19 @@ RSpec.describe 'New/edit issue', :js do
project.add_guest(guest)
end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is missing', label: 'Incident', identifier: :incident
end
context 'when user is reporter' do
let_it_be(:reporter) { create(:user) }
let(:current_user) { reporter }
before_all do
project.add_reporter(reporter)
end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
end
......
......@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe 'Issue Detail', :js do
let_it_be_with_refind(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) }
let(:incident) { create(:incident, project: project, author: user) }
......@@ -90,7 +91,13 @@ RSpec.describe 'Issue Detail', :js do
end
describe 'user updates `issue_type` via the issue type dropdown' do
context 'when an issue `issue_type` is edited by a signed in user' do
let_it_be(:reporter) { create(:user) }
before_all do
project.add_reporter(reporter)
end
describe 'when an issue `issue_type` is edited' do
before do
sign_in(user)
......@@ -98,18 +105,33 @@ RSpec.describe 'Issue Detail', :js do
wait_for_requests
end
it 'routes the user to the incident details page when the `issue_type` is set to incident' do
open_issue_edit_form
context 'by non-member author' do
it 'cannot see Incident option' do
open_issue_edit_form
page.within('[data-testid="issuable-form"]') do
expect(page).to have_content('Issue')
expect(page).not_to have_content('Incident')
end
end
end
context 'by reporter' do
let(:user) { reporter }
page.within('[data-testid="issuable-form"]') do
update_type_select('Issue', 'Incident')
it 'routes the user to the incident details page when the `issue_type` is set to incident' do
open_issue_edit_form
expect(page).to have_current_path(project_issues_incident_path(project, issue))
page.within('[data-testid="issuable-form"]') do
update_type_select('Issue', 'Incident')
expect(page).to have_current_path(project_issues_incident_path(project, issue))
end
end
end
end
context 'when an incident `issue_type` is edited by a signed in user' do
describe 'when an incident `issue_type` is edited' do
before do
sign_in(user)
......@@ -117,13 +139,29 @@ RSpec.describe 'Issue Detail', :js do
wait_for_requests
end
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
open_issue_edit_form
context 'by non-member author' do
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
open_issue_edit_form
page.within('[data-testid="issuable-form"]') do
update_type_select('Incident', 'Issue')
expect(page).to have_current_path(project_issue_path(project, incident))
end
end
end
context 'by reporter' do
let(:user) { reporter }
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
open_issue_edit_form
page.within('[data-testid="issuable-form"]') do
update_type_select('Incident', 'Issue')
page.within('[data-testid="issuable-form"]') do
update_type_select('Incident', 'Issue')
expect(page).to have_current_path(project_issue_path(project, incident))
expect(page).to have_current_path(project_issue_path(project, incident))
end
end
end
end
......
......@@ -171,7 +171,7 @@ RSpec.describe "User creates issue" do
end
context 'form create handles issue creation by default' do
let(:project) { create(:project) }
let_it_be(:project) { create(:project) }
before do
visit new_project_issue_path(project)
......@@ -187,30 +187,22 @@ RSpec.describe "User creates issue" do
end
context 'form create handles incident creation' do
let(:project) { create(:project) }
let_it_be(:project) { create(:project) }
before do
visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } })
end
it 'pre-fills the issue type dropdown with incident type' do
expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Incident')
end
it 'hides the epic select' do
expect(page).not_to have_selector('.epic-dropdown-container')
it 'does not pre-fill the issue type dropdown with incident type' do
expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).not_to have_content('Incident')
end
it 'shows the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
end
it 'hides the weight input' do
expect(page).not_to have_selector('.qa-issuable-weight-input') # rubocop:disable QA/SelectorUsage
end
it 'shows the incident help text' do
expect(page).to have_text('A modified issue to guide the resolution of incidents.')
it 'hides the incident help text' do
expect(page).not_to have_text('A modified issue to guide the resolution of incidents.')
end
end
......@@ -242,6 +234,44 @@ RSpec.describe "User creates issue" do
end
end
context 'when signed in as reporter', :js do
let_it_be(:project) { create(:project) }
before_all do
project.add_reporter(user)
end
before do
sign_in(user)
end
context 'form create handles incident creation' do
before do
visit new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } })
end
it 'pre-fills the issue type dropdown with incident type' do
expect(find('.js-issuable-type-filter-dropdown-wrap .dropdown-toggle-text')).to have_content('Incident')
end
it 'hides the epic select' do
expect(page).not_to have_selector('.epic-dropdown-container')
end
it 'shows the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
end
it 'hides the weight input' do
expect(page).not_to have_selector('.qa-issuable-weight-input') # rubocop:disable QA/SelectorUsage
end
it 'shows the incident help text' do
expect(page).to have_text('A modified issue to guide the resolution of incidents.')
end
end
end
context "when signed in as user with special characters in their name" do
let(:user_special) { create(:user, name: "Jon O'Shea") }
......
......@@ -78,6 +78,7 @@ describe('Incidents List', () => {
authorUsernameQuery: '',
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
canCreateIncident: true,
...provide,
},
stubs: {
......@@ -105,21 +106,23 @@ describe('Incidents List', () => {
describe('empty state', () => {
const {
emptyState: { title, emptyClosedTabTitle, description },
emptyState: { title, emptyClosedTabTitle, description, cannotCreateIncidentDescription },
} = I18N;
it.each`
statusFilter | all | closed | expectedTitle | expectedDescription
${'all'} | ${2} | ${1} | ${title} | ${description}
${'open'} | ${2} | ${0} | ${title} | ${description}
${'closed'} | ${0} | ${0} | ${title} | ${description}
${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${undefined}
statusFilter | all | closed | expectedTitle | canCreateIncident | expectedDescription
${'all'} | ${2} | ${1} | ${title} | ${true} | ${description}
${'open'} | ${2} | ${0} | ${title} | ${true} | ${description}
${'closed'} | ${0} | ${0} | ${title} | ${true} | ${description}
${'closed'} | ${2} | ${0} | ${emptyClosedTabTitle} | ${true} | ${undefined}
${'all'} | ${2} | ${1} | ${title} | ${false} | ${cannotCreateIncidentDescription}
`(
`when active tab is $statusFilter and there are $all incidents in total and $closed closed incidents, the empty state
has title: $expectedTitle and description: $expectedDescription`,
({ statusFilter, all, closed, expectedTitle, expectedDescription }) => {
({ statusFilter, all, closed, expectedTitle, expectedDescription, canCreateIncident }) => {
mountComponent({
data: { incidents: { list: [] }, incidentsCount: { all, closed }, statusFilter },
provide: { canCreateIncident },
loading: false,
});
expect(findEmptyState().exists()).toBe(true);
......@@ -219,6 +222,15 @@ describe('Incidents List', () => {
expect(findCreateIncidentBtn().exists()).toBe(false);
});
it("doesn't show the button when user does not have incident creation permissions", () => {
mountComponent({
data: { incidents: { list: mockIncidents }, incidentsCount: {} },
provide: { canCreateIncident: false },
loading: false,
});
expect(findCreateIncidentBtn().exists()).toBe(false);
});
it('should track create new incident button', async () => {
findCreateIncidentBtn().vm.$emit('click');
await wrapper.vm.$nextTick();
......
......@@ -39,7 +39,7 @@ describe('Issue type field component', () => {
const findTypeFromDropDownItemIconAt = (at) =>
findTypeFromDropDownItems().at(at).findComponent(GlIcon);
const createComponent = ({ data } = {}) => {
const createComponent = ({ data } = {}, provide) => {
fakeApollo = createMockApollo([], mockResolvers);
wrapper = shallowMount(IssueTypeField, {
......@@ -51,6 +51,10 @@ describe('Issue type field component', () => {
...data,
};
},
provide: {
canCreateIncident: true,
...provide,
},
});
};
......@@ -92,5 +96,25 @@ describe('Issue type field component', () => {
await wrapper.vm.$nextTick();
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
});
describe('when user is a guest', () => {
it('hides the incident type from the dropdown', async () => {
createComponent({}, { canCreateIncident: false, issueType: 'issue' });
await waitForPromises();
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
createComponent({}, { canCreateIncident: false, issueType: 'incident' });
await waitForPromises();
expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
});
});
});
});
......@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::IncidentsHelper do
include Gitlab::Routing.url_helpers
let(:project) { create(:project) }
let(:user) { build_stubbed(:user) }
let(:project) { build_stubbed(:project) }
let(:project_path) { project.full_path }
let(:new_issue_path) { new_project_issue_path(project) }
let(:issue_path) { project_issues_path(project) }
......@@ -17,21 +18,43 @@ RSpec.describe Projects::IncidentsHelper do
}
end
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?)
.with(user, :create_incident, project)
.and_return(can_create_incident)
end
describe '#incidents_data' do
subject(:data) { helper.incidents_data(project, params) }
it 'returns frontend configuration' do
expect(data).to include(
'project-path' => project_path,
'new-issue-path' => new_issue_path,
'incident-template-name' => 'incident',
'incident-type' => 'incident',
'issue-path' => issue_path,
'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
'text-query': 'search text',
'author-username-query': 'root',
'assignee-username-query': 'max.power'
)
shared_examples 'frontend configuration' do
it 'returns frontend configuration' do
expect(data).to include(
'project-path' => project_path,
'new-issue-path' => new_issue_path,
'incident-template-name' => 'incident',
'incident-type' => 'incident',
'issue-path' => issue_path,
'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
'text-query': 'search text',
'author-username-query': 'root',
'assignee-username-query': 'max.power',
'can-create-incident': can_create_incident.to_s
)
end
end
context 'when user can create incidents' do
let(:can_create_incident) { true }
include_examples 'frontend configuration'
end
context 'when user cannot create incidents' do
let(:can_create_incident) { false }
include_examples 'frontend configuration'
end
end
end
......@@ -7,12 +7,14 @@ RSpec.describe Issues::BuildService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:user) { developer }
before_all do
project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest)
end
......@@ -177,7 +179,8 @@ RSpec.describe Issues::BuildService do
where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do
nil | ref(:guest) | ref(:type_issue_id) | 'issue'
'issue' | ref(:guest) | ref(:type_issue_id) | 'issue'
'incident' | ref(:guest) | ref(:type_incident_id) | 'incident'
'incident' | ref(:guest) | ref(:type_issue_id) | 'issue'
'incident' | ref(:reporter) | ref(:type_incident_id) | 'incident'
# update once support for test_case is enabled
'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue'
# update once support for requirement is enabled
......
......@@ -80,7 +80,7 @@ RSpec.describe Issues::CreateService do
it_behaves_like 'not an incident issue'
context 'issue is incident type' do
context 'when issue is incident type' do
before do
opts.merge!(issue_type: 'incident')
end
......@@ -90,23 +90,37 @@ RSpec.describe Issues::CreateService do
subject { issue }
it_behaves_like 'incident issue'
it_behaves_like 'has incident label'
context 'as reporter' do
let_it_be(:reporter) { create(:user) }
it 'does create an incident label' do
expect { subject }
.to change { Label.where(incident_label_attributes).count }.by(1)
end
let(:user) { reporter }
context 'when invalid' do
before do
opts.merge!(title: '')
before_all do
project.add_reporter(reporter)
end
it_behaves_like 'incident issue'
it_behaves_like 'has incident label'
it 'does create an incident label' do
expect { subject }
.to change { Label.where(incident_label_attributes).count }.by(1)
end
it 'does not apply an incident label prematurely' do
expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count)
context 'when invalid' do
before do
opts.merge!(title: '')
end
it 'does not apply an incident label prematurely' do
expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count)
end
end
end
context 'as guest' do
it_behaves_like 'not an incident issue'
end
end
it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
......
......@@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_guest_permissions) do
%i[
award_emoji create_issue create_incident create_merge_request_in create_note
award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link
read_label read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
......@@ -25,10 +25,11 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_reporter_permissions) do
%i[
admin_issue admin_issue_link admin_label admin_issue_board_list create_snippet
daily_statistics download_code download_wiki_code fork_project metrics_dashboard
read_build read_commit_status read_confidential_issues
read_container_image read_deployment read_environment read_merge_request
admin_issue admin_issue_link admin_label admin_issue_board_list
create_snippet create_incident daily_statistics download_code
download_wiki_code fork_project metrics_dashboard read_build
read_commit_status read_confidential_issues read_container_image
read_deployment read_environment read_merge_request
read_metrics_dashboard_annotation read_pipeline read_prometheus
read_sentry_issue update_issue
]
......
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