diff options
Diffstat (limited to 'spec/services/integrations/slack_interactions')
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 |