summaryrefslogtreecommitdiff
path: root/spec/services/integrations/slack_interactions
diff options
context:
space:
mode:
Diffstat (limited to 'spec/services/integrations/slack_interactions')
-rw-r--r--spec/services/integrations/slack_interactions/block_action_service_spec.rb48
-rw-r--r--spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb78
-rw-r--r--spec/services/integrations/slack_interactions/incident_management/incident_modal_opened_service_spec.rb141
-rw-r--r--spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb296
-rw-r--r--spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb158
5 files changed, 721 insertions, 0 deletions
diff --git a/spec/services/integrations/slack_interactions/block_action_service_spec.rb b/spec/services/integrations/slack_interactions/block_action_service_spec.rb
new file mode 100644
index 00000000000..9a188ddcfe4
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/block_action_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::BlockActionService, feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:params) do
+ {
+ view: {
+ team_id: slack_installation.team_id
+ },
+ actions: [{
+ action_id: action_id
+ }]
+ }
+ end
+
+ subject(:execute) { described_class.new(params).execute }
+
+ context 'when action_id is incident_management_project' do
+ let(:action_id) { 'incident_management_project' }
+
+ it 'executes the correct handler' do
+ project_handler = described_class::ALLOWED_UPDATES_HANDLERS['incident_management_project']
+
+ expect_next_instance_of(project_handler, params, params[:actions].first) do |handler|
+ expect(handler).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when action_id is not known' do
+ let(:action_id) { 'random' }
+
+ it 'does not execute the handlers' do
+ described_class::ALLOWED_UPDATES_HANDLERS.each_value do |handler_class|
+ expect(handler_class).not_to receive(:new)
+ end
+
+ execute
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb b/spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb
new file mode 100644
index 00000000000..64cddf9a66b
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::IncidentManagement::IncidentModalClosedService,
+ feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:request_body) do
+ {
+ replace_original: 'true',
+ text: 'Incident creation cancelled.'
+ }
+ end
+
+ let(:params) do
+ {
+ view: {
+ private_metadata: 'https://api.slack.com/id/1234'
+ }
+ }
+ end
+
+ let(:service) { described_class.new(params) }
+
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return({ ok: true })
+ end
+
+ context 'when executed' do
+ it 'makes the POST call and closes the modal' do
+ expect(Gitlab::HTTP).to receive(:post).with(
+ 'https://api.slack.com/id/1234',
+ body: Gitlab::Json.dump(request_body),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+
+ service.execute
+ end
+ end
+
+ context 'when the POST call raises an HTTP exception' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
+ end
+
+ it 'tracks the exception and returns an error response' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ Errno::ECONNREFUSED.new('HTTP exception when calling Slack API'),
+ {
+ params: params
+ }
+ )
+
+ service.execute
+ end
+ end
+
+ context 'when response is not ok' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return({ ok: false })
+ end
+
+ it 'returns error response and tracks the exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Something went wrong while closing the incident form.'),
+ {
+ response: { ok: false },
+ params: params
+ }
+ )
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interactions/incident_management/incident_modal_opened_service_spec.rb b/spec/services/integrations/slack_interactions/incident_management/incident_modal_opened_service_spec.rb
new file mode 100644
index 00000000000..2a1aa0aabec
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/incident_management/incident_modal_opened_service_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::IncidentManagement::IncidentModalOpenedService,
+ 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, developer_projects: [project]) }
+ let_it_be(:trigger_id) { '12345.98765.abcd2358fdea' }
+
+ let(:slack_workspace_id) { slack_installation.team_id }
+ let(:response_url) { 'https://api.slack.com/id/123' }
+ let(:api_url) { "#{Slack::API::BASE_URL}/views.open" }
+ let(:mock_modal) { { type: 'modal', blocks: [] } }
+ let(:params) do
+ {
+ team_id: slack_workspace_id,
+ response_url: response_url,
+ trigger_id: trigger_id
+ }
+ end
+
+ before do
+ response = {
+ id: '123',
+ state: {
+ values: {
+ project_and_severity_selector: {
+ incident_management_project: {
+ selected_option: {
+ value: project.id.to_s
+ }
+ }
+ }
+ }
+ }
+ }
+ stub_request(:post, api_url)
+ .to_return(
+ status: 200,
+ body: Gitlab::Json.dump({ ok: true, view: response }),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ subject { described_class.new(slack_installation, user, params) }
+
+ context 'when triggered' do
+ it 'opens the modal' do
+ expect_next_instance_of(Slack::BlockKit::IncidentManagement::IncidentModalOpened) do |ui|
+ expect(ui).to receive(:build).and_return(mock_modal)
+ end
+
+ expect(Rails.cache).to receive(:write).with(
+ 'slack:incident_modal_opened:123', project.id.to_s, { expires_in: 5.minutes })
+
+ response = subject.execute
+
+ expect(WebMock).to have_requested(:post, api_url).with(
+ body: {
+ trigger_id: trigger_id,
+ view: mock_modal
+ },
+ headers: {
+ 'Authorization' => "Bearer #{slack_installation.bot_access_token}",
+ 'Content-Type' => 'application/json; charset=utf-8'
+ })
+
+ expect(response.message).to eq('Please complete the incident creation form.')
+ end
+ end
+
+ context 'when there are no projects with slack integration' do
+ let(:params) do
+ {
+ team_id: 'some_random_id',
+ response_url: response_url,
+ trigger_id: trigger_id
+ }
+ end
+
+ let(:user) { create(:user) }
+
+ it 'does not open the modal' do
+ response = subject.execute
+
+ expect(Rails.cache).not_to receive(:write)
+ expect(response.message).to be('You do not have access to any projects for creating incidents.')
+ end
+ end
+
+ context 'when Slack API call raises an HTTP exception' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
+ end
+
+ it 'tracks the exception and returns an error response' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ Errno::ECONNREFUSED.new('HTTP exception when calling Slack API'),
+ {
+ slack_workspace_id: slack_workspace_id
+ }
+ )
+
+ expect(Rails.cache).not_to receive(:write)
+ expect(subject.execute).to be_error
+ end
+ end
+
+ context 'when api returns an error' do
+ before do
+ stub_request(:post, api_url)
+ .to_return(
+ status: 404,
+ body: Gitlab::Json.dump({ ok: false }),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ it 'returns error when called' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Something went wrong while opening the incident form.'),
+ {
+ response: { "ok" => false },
+ slack_workspace_id: slack_workspace_id,
+ slack_user_id: slack_installation.user_id
+ }
+ )
+
+ expect(Rails.cache).not_to receive(:write)
+ response = subject.execute
+
+ expect(response.message).to eq('Something went wrong while opening the incident form.')
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb b/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb
new file mode 100644
index 00000000000..adaeadaa997
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb
@@ -0,0 +1,296 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::IncidentManagement::IncidentModalSubmitService,
+ feature_category: :incident_management do
+ include Gitlab::Routing
+
+ 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
+ )
+ end
+
+ # Setting below params as they are optional, have added values wherever required in specs
+ let(:zoom_link) { '' }
+ let(:severity) { {} }
+ let(:status) { '' }
+ let(:assignee_id) { nil }
+ let(:selected_label_ids) { [] }
+ let(:label_ids) { { selected_options: selected_label_ids } }
+ 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: {
+ incident_management_project: {
+ selected_option: {
+ value: project.id.to_s
+ }
+ },
+ severity: severity
+ },
+ confidentiality: {
+ confidential: confidential
+ },
+ zoom: zoom,
+ status_and_assignee_selector: {
+ status: {
+ selected_option: {
+ value: status
+ }
+ },
+ assignee: {
+ selected_option: {
+ value: assignee_id
+ }
+ }
+ },
+ label_selector: {
+ labels: label_ids
+ }
+ }
+ }
+ }
+ }
+ 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 markup string in title' do
+ let(:title) { '<a href="url">incident title</a>' }
+ let(:incident) { create(:incident, title: title, project: project) }
+
+ before do
+ allow_next_instance_of(Issues::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(
+ ServiceResponse.success(payload: { issue: incident, error: [] })
+ )
+ end
+ end
+
+ it 'strips the markup and saves sends the title' do
+ expect(Gitlab::HTTP).to receive(:post)
+ .with(
+ api_url,
+ body: Gitlab::Json.dump(
+ {
+ replace_original: 'true',
+ text: "New incident has been created: " \
+ "<#{issue_url(incident)}|#{incident.to_reference} - a href=\"url\"incident title/a>. "
+ }
+ ),
+ headers: { 'Content-Type' => 'application/json' }
+ ).and_return(api_response)
+
+ execute_service
+ end
+ 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
+ expect(incident.escalation_status).to be_triggered
+ 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 'with incident status' do
+ let(:status) { 'resolved' }
+
+ it 'sets the incident status' do
+ incident = execute_service[:incident]
+
+ expect(incident.escalation_status).to be_resolved
+ end
+ end
+
+ context 'with assignee id' do
+ let(:assignee_id) { user.id.to_s }
+
+ it 'assigns the incident to user' do
+ incident = execute_service[:incident]
+
+ expect(incident.assignees).to contain_exactly(user)
+ end
+
+ context 'when user is not a member of the project' do
+ let(:assignee_id) { create(:user).id.to_s }
+
+ it 'does not assign the user' do
+ incident = execute_service[:incident]
+
+ expect(incident.assignees).to be_empty
+ end
+ end
+ end
+
+ context 'with label ids' do
+ let_it_be(:project_label1) { create(:label, project: project, title: 'Label 1') }
+ let_it_be(:project_label2) { create(:label, project: project, title: 'Label 2') }
+
+ let(:selected_label_ids) do
+ [
+ { value: project_label1.id.to_s },
+ { value: project_label2.id.to_s }
+ ]
+ end
+
+ it 'assigns the label to the incident' do
+ incident = execute_service[:incident]
+
+ expect(incident.labels).to contain_exactly(project_label1, project_label2)
+ 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/spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb b/spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb
new file mode 100644
index 00000000000..5edffc99977
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::SlackBlockActions::IncidentManagement::ProjectUpdateHandler,
+ feature_category: :incident_management do
+ describe '#execute' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+ let_it_be(:old_project) { create(:project) }
+ let_it_be(:new_project) { create(:project) }
+ let_it_be(:user) { create(:user, developer_projects: [old_project, new_project]) }
+ let_it_be(:chat_name) { create(:chat_name, user: user) }
+ let_it_be(:api_url) { "#{Slack::API::BASE_URL}/views.update" }
+
+ let(:block) do
+ {
+ block_id: 'incident_description',
+ element: {
+ initial_value: ''
+ }
+ }
+ end
+
+ let(:view) do
+ {
+ id: 'V04EQH1SP27',
+ team_id: slack_installation.team_id,
+ blocks: [block]
+ }
+ end
+
+ let(:action) do
+ {
+ selected_option: {
+ value: new_project.id.to_s
+ }
+ }
+ end
+
+ let(:params) do
+ {
+ view: view,
+ user: {
+ id: slack_installation.user_id
+ }
+ }
+ end
+
+ before do
+ allow_next_instance_of(ChatNames::FindUserService) do |user_service|
+ allow(user_service).to receive(:execute).and_return(chat_name)
+ end
+
+ stub_request(:post, api_url)
+ .to_return(
+ status: 200,
+ body: Gitlab::Json.dump({ ok: true }),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ shared_examples 'does not make api call' do
+ it 'does not make the api call and returns nil' do
+ expect(Rails.cache).to receive(:read).and_return(project.id.to_s)
+ expect(Rails.cache).not_to receive(:write)
+
+ expect(execute).to be_nil
+ expect(WebMock).not_to have_requested(:post, api_url)
+ end
+ end
+
+ subject(:execute) { described_class.new(params, action).execute }
+
+ context 'when project is updated' do
+ it 'returns success response and updates cache' do
+ expect(Rails.cache).to receive(:read).and_return(old_project.id.to_s)
+ expect(Rails.cache).to receive(:write).with(
+ "slack:incident_modal_opened:#{view[:id]}",
+ new_project.id.to_s,
+ expires_in: 5.minutes
+ )
+
+ expect(execute.message).to eq('Modal updated')
+
+ updated_block = block.dup
+ updated_block[:block_id] = new_project.id.to_s
+ view[:blocks] = [updated_block]
+
+ expect(WebMock).to have_requested(:post, api_url).with(
+ body: {
+ view_id: view[:id],
+ view: view.except!(:team_id, :id)
+ },
+ headers: {
+ 'Authorization' => "Bearer #{slack_installation.bot_access_token}",
+ 'Content-Type' => 'application/json; charset=utf-8'
+ })
+ end
+ end
+
+ context 'when project is unchanged' do
+ it_behaves_like 'does not make api call' do
+ let(:project) { new_project }
+ end
+ end
+
+ context 'when user does not have permission to read a project' do
+ it_behaves_like 'does not make api call' do
+ let(:project) { create(:project) }
+ end
+ end
+
+ context 'when api response is not ok' do
+ before do
+ stub_request(:post, api_url)
+ .to_return(
+ status: 404,
+ body: Gitlab::Json.dump({ ok: false }),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ it 'returns error response' do
+ expect(Rails.cache).to receive(:read).and_return(old_project.id.to_s)
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Something went wrong while updating the modal.'),
+ {
+ response: { "ok" => false },
+ slack_workspace_id: slack_installation.team_id,
+ slack_user_id: slack_installation.user_id
+ }
+ )
+
+ expect(execute.message).to eq('Something went wrong while updating the modal.')
+ end
+ end
+
+ context 'when Slack API call raises an HTTP exception' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
+ end
+
+ it 'tracks the exception and returns an error message' do
+ expect(Rails.cache).to receive(:read).and_return(old_project.id.to_s)
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ Errno::ECONNREFUSED.new('HTTP exception when calling Slack API'),
+ {
+ slack_workspace_id: slack_installation.team_id
+ }
+ )
+
+ expect(execute).to be_error
+ end
+ end
+ end
+end