diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb index 3dd3ba7f01c4c51ab4ecae1c609f58baeed41701..ba6ebb7206bfa278c9a5633f8284d3dea3ee1684 100644 --- a/app/services/chat_names/find_user_service.rb +++ b/app/services/chat_names/find_user_service.rb @@ -19,6 +19,13 @@ def execute # rubocop: disable CodeReuse/ActiveRecord def find_chat_name + if @integration.nil? + return ChatName.find_by( + team_id: @params[:team_id], + chat_id: @params[:user_id] + ) + end + ChatName.find_by( integration: @integration, team_id: @params[:team_id], diff --git a/ee/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb b/ee/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..f55fd0ed033e1c7b75f69a109786c025809f6292 --- /dev/null +++ b/ee/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +module Integrations + module SlackInteractions + module IncidentManagement + class IncidentModalSubmitService + include GitlabRoutingHelper + include Gitlab::Routing + + IssueCreateError = Class.new(StandardError) + + def initialize(params) + @params = params + @values = params.dig(:view, :state, :values) + @team_id = params.dig(:team, :id) + @user_id = params.dig(:user, :id) + end + + attr_accessor :params, :values, :team_id, :user_id + + def execute + create_response = Issues::CreateService.new( + project: project, + current_user: find_user.user, + params: incident_params, + spam_params: nil + ).execute + + raise IssueCreateError, create_response.errors.to_sentence if create_response.error? + + incident = create_response.payload[:issue] + incident_link = incident_link_text(incident) + response = send_to_slack(incident_link) + + return ServiceResponse.success(payload: { incident: incident }) if response['ok'] + + ServiceResponse.error( + message: _('Something went wrong when sending the incident link to Slack.'), + payload: response + ).track_exception( + response: response.to_h, + slack_workspace_id: team_id, + slack_user_id: user_id + ) + rescue StandardError => e + send_to_slack(_('There was a problem creating the incident. Please try again.')) + + ServiceResponse + .error( + message: e.message + ).track_exception( + slack_workspace_id: team_id, + slack_user_id: user_id, + as: e.class + ) + end + + private + + def incident_params + { + "title": values.dig(:title_input, :title, :value), + "severity": severity, + "confidential": confidential?, + "description": description, + "issue_type": "incident" + } + end + + def send_to_slack(text) + response_url = params.dig(:view, :private_metadata) + + body = { + 'replace_original': 'true', + 'text': text + } + + Gitlab::HTTP.post( + response_url, + body: Gitlab::Json.dump(body), + headers: { 'Content-Type' => 'application/json' } + ) + end + + def incident_link_text(incident) + "#{_('New incident has been created')}: <#{issue_url(incident)}|#{incident.to_reference} - #{incident.title}>" + end + + def project + full_path = values.dig(:project_and_severity_selector, :project, :selected_option, :value) + + Project.find_by_full_path(full_path) + end + + def find_user + ChatNames::FindUserService.new( + nil, + { team_id: team_id, user_id: user_id } + ).execute + end + + def description + description = values.dig(:incident_description, :description, :value) + zoom_link = values.dig(:zoom, :link, :value) + + return description if zoom_link.blank? + + "#{description} \n/zoom #{zoom_link}" + end + + def confidential? + values.dig(:confidentiality, :confidential, :selected_options).present? + end + + def severity + values.dig(:project_and_severity_selector, :severity, :selected_option, :value) || 'unknown' + end + end + end + end +end diff --git a/ee/app/workers/integrations/slack_interactivity_worker.rb b/ee/app/workers/integrations/slack_interactivity_worker.rb index 8ef16ae4734bf0f0670bf95df4a832d628f3517c..5223f3fe058a5c517b172784b6c12402d945ec84 100644 --- a/ee/app/workers/integrations/slack_interactivity_worker.rb +++ b/ee/app/workers/integrations/slack_interactivity_worker.rb @@ -10,7 +10,8 @@ class SlackInteractivityWorker # rubocop:disable Scalability/IdempotentWorker include ApplicationWorker INTERACTIONS = { - 'view_closed' => SlackInteractions::IncidentManagement::IncidentModalClosedService + 'view_closed' => SlackInteractions::IncidentManagement::IncidentModalClosedService, + 'view_submission' => SlackInteractions::IncidentManagement::IncidentModalSubmitService }.freeze feature_category :integrations diff --git a/ee/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb b/ee/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9349cb5d46c0f149252f1263571fd82486c6e75a --- /dev/null +++ b/ee/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::SlackInteractions::IncidentManagement::IncidentModalSubmitService, + feature_category: :incident_management do + describe '#execute' do + let_it_be(:slack_installation) { create(:slack_integration) } + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:api_url) { 'https://api.slack.com/id/1234' } + + let_it_be(:chat_name) do + create(:chat_name, + user: user, + team_id: slack_installation.team_id, + chat_id: slack_installation.user_id, + integration: slack_installation.integration + ) + end + + # Setting below params as they are optional, have added values wherever required in specs + let(:zoom_link) { '' } + let(:severity) { {} } + let(:confidential_selected_options) { [] } + let(:confidential) { { selected_options: confidential_selected_options } } + let(:title) { 'Incident title' } + + let(:zoom) do + { + link: { + value: zoom_link + } + } + end + + let(:params) do + { + team: { + id: slack_installation.team_id + }, + user: { + id: slack_installation.user_id + }, + view: { + private_metadata: api_url, + state: { + values: { + title_input: { + title: { + value: title + } + }, + incident_description: { + description: { + value: 'Incident description' + } + }, + project_and_severity_selector: { + project: { + selected_option: { + value: project.full_path + } + }, + severity: severity + }, + confidentiality: { + confidential: confidential + }, + zoom: zoom + } + } + } + } + end + + subject(:execute_service) { described_class.new(params).execute } + + shared_examples 'error in creation' do |error_message| + it 'returns error and raises exception' do + expect(::Gitlab::ErrorTracking).to receive(:track_exception) + .with( + described_class::IssueCreateError.new(error_message), + { + slack_workspace_id: slack_installation.team_id, + slack_user_id: slack_installation.user_id + } + ) + + expect(Gitlab::HTTP).to receive(:post) + .with( + api_url, + body: Gitlab::Json.dump( + { + 'replace_original': 'true', + 'text': 'There was a problem creating the incident. Please try again.' + } + ), + headers: { 'Content-Type' => 'application/json' } + ) + + response = execute_service + + expect(response).to be_error + expect(response.message).to eq(error_message) + end + end + + context 'when user has permissions to create incidents' do + let(:api_response) { '{"ok":true}' } + + before do + project.add_developer(user) + stub_request(:post, api_url) + .to_return(body: api_response, headers: { 'Content-Type' => 'application/json' }) + end + + context 'with non-optional params' do + it 'creates incident' do + response = execute_service + incident = response[:incident] + + expect(response).to be_success + expect(incident).not_to be_nil + expect(incident.description).to eq('Incident description') + expect(incident.author).to eq(user) + expect(incident.severity).to eq('unknown') + expect(incident.confidential).to be_falsey + end + + it 'sends incident link to slack' do + execute_service + + expect(WebMock).to have_requested(:post, api_url) + end + end + + context 'with zoom_link' do + let(:zoom_link) { 'https://gitlab.zoom.us/j/1234' } + + it 'sets zoom link as quick action' do + incident = execute_service[:incident] + zoom_meeting = ZoomMeeting.find_by_issue_id(incident.id) + + expect(incident.description).to eq("Incident description") + expect(zoom_meeting.url).to eq(zoom_link) + end + end + + context 'with confidential and severity' do + let(:confidential_selected_options) { ['confidential'] } + let(:severity) do + { + selected_option: { + value: 'high' + } + } + end + + it 'sets confidential and severity' do + incident = execute_service[:incident] + + expect(incident.confidential).to be_truthy + expect(incident.severity).to eq('high') + end + end + + context 'when response is not ok' do + let(:api_response) { '{"ok":false}' } + + it 'returns error response and tracks the exception' do + expect(::Gitlab::ErrorTracking).to receive(:track_exception) + .with( + StandardError.new('Something went wrong when sending the incident link to Slack.'), + { + response: { 'ok' => false }, + slack_workspace_id: slack_installation.team_id, + slack_user_id: slack_installation.user_id + } + ) + + execute_service + end + end + + context 'when incident creation fails' do + let(:title) { '' } + + it_behaves_like 'error in creation', "Title can't be blank" + end + end + + context 'when user does not have permission to create incidents' do + it_behaves_like 'error in creation', 'Operation not allowed' + end + end +end diff --git a/ee/spec/workers/integrations/slack_interactivity_worker_spec.rb b/ee/spec/workers/integrations/slack_interactivity_worker_spec.rb index 4195e0a1641613282c9aad58642ca61b362447db..99f3ecfba70b8c8bcacda20174dce9bc9eaa5288 100644 --- a/ee/spec/workers/integrations/slack_interactivity_worker_spec.rb +++ b/ee/spec/workers/integrations/slack_interactivity_worker_spec.rb @@ -3,19 +3,23 @@ require 'spec_helper' RSpec.describe Integrations::SlackInteractivityWorker, :clean_gitlab_redis_shared_state do - describe '.interaction?' do - subject { described_class.interaction?(slack_interaction) } - - context 'when slack_interaction is known' do - let(:slack_interaction) { 'view_closed' } + using RSpec::Parameterized::TableSyntax - it { is_expected.to be_truthy } - end + let_it_be(:slack_integration) { create(:slack_integration) } - context 'when slack_interaction is not known' do - let(:slack_interaction) { 'foo' } + describe '.interaction?' do + context 'when slack_interaction is known/unknown' do + where(:slack_interaction, :result) do + 'view_closed' | true + 'view_submission' | true + 'foo' | false + end - it { is_expected.to be_falsey } + with_them do + it 'returns correct result' do + expect(described_class.interaction?(slack_interaction)).to be(result) + end + end end end @@ -30,8 +34,6 @@ end let(:worker) { described_class.new } - let(:slack_interaction) { 'view_closed' } - let(:service_class) { ::Integrations::SlackInteractions::IncidentManagement::IncidentModalClosedService } let(:args) do { @@ -43,13 +45,16 @@ let(:params) do { user: { - id: 'U0123ABCDEF' + id: slack_integration.user_id }, team: { - id: 'T0123A456BC' + id: slack_integration.team_id }, view: { - private_metadata: 'https://response.slack.com/id/123' + private_metadata: 'https://response.slack.com/id/123', + state: { + values: {} + } } } end @@ -57,28 +62,52 @@ shared_examples 'logs extra metadata on done' do specify do expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_interaction, slack_interaction) - expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_user_id, 'U0123ABCDEF') - expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_workspace_id, 'T0123A456BC') + expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_user_id, slack_integration.user_id) + expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_workspace_id, slack_integration.team_id) worker.perform(args) end end - it 'executes the correct service' do - expect_next_instance_of(service_class, params) do |service| - expect(service).to receive(:execute).and_return(ServiceResponse.success) + context 'when view is closed' do + let(:slack_interaction) { 'view_closed' } + + it 'executes the correct service' do + view_closed_service = described_class::INTERACTIONS['view_closed'] + + expect_next_instance_of(view_closed_service, params) do |service| + expect(service).to receive(:execute).and_return(ServiceResponse.success) + end + + worker.perform(args) end - worker.perform(args) + it_behaves_like 'logs extra metadata on done' end - it_behaves_like 'logs extra metadata on done' + context 'when view is submitted' do + let(:slack_interaction) { 'view_submission' } + + it 'executes the submission service' do + view_submission_service = described_class::INTERACTIONS['view_submission'] + + expect_next_instance_of(view_submission_service, params) do |service| + expect(service).to receive(:execute).and_return(ServiceResponse.success) + end + + worker.perform(args) + end + + it_behaves_like 'logs extra metadata on done' + end context 'when slack_interaction is not known' do let(:slack_interaction) { 'foo' } - it 'does not execute the service class' do - expect(service_class).not_to receive(:new) + it 'does not execute a service class' do + described_class::INTERACTIONS.each_value do |service_class| + expect(service_class).not_to receive(:new) + end worker.perform(args) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ca3b0449826f04ec5bb04b8fd1e7c25561e3427c..0256549ddbe2f347a6b9f2681f71ce774e70d044 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -27129,6 +27129,9 @@ msgstr "" msgid "New incident" msgstr "" +msgid "New incident has been created" +msgstr "" + msgid "New issue" msgstr "" @@ -38676,6 +38679,9 @@ msgstr "" msgid "Something went wrong when reordering designs. Please try again" msgstr "" +msgid "Something went wrong when sending the incident link to Slack." +msgstr "" + msgid "Something went wrong while adding timeline event." msgstr "" @@ -41497,6 +41503,9 @@ msgstr "" msgid "There was a problem communicating with your device." msgstr "" +msgid "There was a problem creating the incident. Please try again." +msgstr "" + msgid "There was a problem fetching CRM contacts." msgstr "" diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb index 4b0a12045587f24226d510871ef1124455a0f2ba..b65a76ca37c99171b8ddfcd032248ab5c1e0b6de 100644 --- a/spec/services/chat_names/find_user_service_spec.rb +++ b/spec/services/chat_names/find_user_service_spec.rb @@ -40,6 +40,14 @@ expect(chat_name.reload.last_used_at).to eq(time) end + + context 'when integration is not passed' do + it 'returns chat name' do + requested_chat_name = described_class.new(nil, params).execute + + expect(requested_chat_name).to eq(chat_name) + end + end end context 'when different user is requested' do