summaryrefslogtreecommitdiff
path: root/spec/services/integrations
diff options
context:
space:
mode:
Diffstat (limited to 'spec/services/integrations')
-rw-r--r--spec/services/integrations/propagate_service_spec.rb2
-rw-r--r--spec/services/integrations/slack_event_service_spec.rb56
-rw-r--r--spec/services/integrations/slack_events/app_home_opened_service_spec.rb113
-rw-r--r--spec/services/integrations/slack_events/url_verification_service_spec.rb11
-rw-r--r--spec/services/integrations/slack_interaction_service_spec.rb70
-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
-rw-r--r--spec/services/integrations/slack_option_service_spec.rb76
-rw-r--r--spec/services/integrations/slack_options/label_search_handler_spec.rb47
-rw-r--r--spec/services/integrations/slack_options/user_search_handler_spec.rb52
-rw-r--r--spec/services/integrations/test/project_service_spec.rb2
14 files changed, 1148 insertions, 2 deletions
diff --git a/spec/services/integrations/propagate_service_spec.rb b/spec/services/integrations/propagate_service_spec.rb
index c971c4a0ad0..0267b1b0ed0 100644
--- a/spec/services/integrations/propagate_service_spec.rb
+++ b/spec/services/integrations/propagate_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::PropagateService do
+RSpec.describe Integrations::PropagateService, feature_category: :integrations do
describe '.propagate' do
include JiraIntegrationHelpers
diff --git a/spec/services/integrations/slack_event_service_spec.rb b/spec/services/integrations/slack_event_service_spec.rb
new file mode 100644
index 00000000000..17433aee329
--- /dev/null
+++ b/spec/services/integrations/slack_event_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackEventService, feature_category: :integrations do
+ describe '#execute' do
+ subject(:execute) { described_class.new(params).execute }
+
+ let(:params) do
+ {
+ type: 'event_callback',
+ event: {
+ type: 'app_home_opened',
+ foo: 'bar'
+ }
+ }
+ end
+
+ it 'queues a worker and returns success response' do
+ expect(Integrations::SlackEventWorker).to receive(:perform_async)
+ .with(
+ {
+ slack_event: 'app_home_opened',
+ params: {
+ event: {
+ foo: 'bar'
+ }
+ }
+ }
+ )
+ expect(execute.payload).to eq({})
+ is_expected.to be_success
+ end
+
+ context 'when event a url verification request' do
+ let(:params) { { type: 'url_verification', foo: 'bar' } }
+
+ it 'executes the service instead of queueing a worker and returns success response' do
+ expect(Integrations::SlackEventWorker).not_to receive(:perform_async)
+ expect_next_instance_of(Integrations::SlackEvents::UrlVerificationService, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return({ baz: 'qux' })
+ end
+ expect(execute.payload).to eq({ baz: 'qux' })
+ is_expected.to be_success
+ end
+ end
+
+ context 'when event is unknown' do
+ let(:params) { super().merge(event: { type: 'foo' }) }
+
+ it 'raises an error' do
+ expect { execute }.to raise_error(described_class::UnknownEventError)
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_events/app_home_opened_service_spec.rb b/spec/services/integrations/slack_events/app_home_opened_service_spec.rb
new file mode 100644
index 00000000000..0eb4c019e0a
--- /dev/null
+++ b/spec/services/integrations/slack_events/app_home_opened_service_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackEvents::AppHomeOpenedService, feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:slack_workspace_id) { slack_installation.team_id }
+ let(:slack_user_id) { 'U0123ABCDEF' }
+ let(:api_url) { "#{Slack::API::BASE_URL}/views.publish" }
+ let(:api_response) { { ok: true } }
+ let(:params) do
+ {
+ team_id: slack_workspace_id,
+ event: { user: slack_user_id },
+ event_id: 'Ev03SA75UJKB'
+ }
+ end
+
+ subject(:execute) { described_class.new(params).execute }
+
+ before do
+ stub_request(:post, api_url)
+ .to_return(
+ status: 200,
+ body: api_response.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ shared_examples 'there is no bot token' do
+ it 'does not call the Slack API, logs info, and returns a success response' do
+ expect(Gitlab::IntegrationsLogger).to receive(:info).with(
+ {
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ message: 'SlackInstallation record has no bot token'
+ }
+ )
+
+ is_expected.to be_success
+ end
+ end
+
+ it 'calls the Slack API correctly and returns a success response' do
+ mock_view = { type: 'home', blocks: [] }
+
+ expect_next_instance_of(Slack::BlockKit::AppHomeOpened) do |ui|
+ expect(ui).to receive(:build).and_return(mock_view)
+ end
+
+ is_expected.to be_success
+
+ expect(WebMock).to have_requested(:post, api_url).with(
+ body: {
+ user_id: slack_user_id,
+ view: mock_view
+ },
+ headers: {
+ 'Authorization' => "Bearer #{slack_installation.bot_access_token}",
+ 'Content-Type' => 'application/json; charset=utf-8'
+ })
+ end
+
+ context 'when the slack installation is a legacy record' do
+ let_it_be(:slack_installation) { create(:slack_integration, :legacy) }
+
+ it_behaves_like 'there is no bot token'
+ end
+
+ context 'when the slack installation cannot be found' do
+ let(:slack_workspace_id) { non_existing_record_id }
+
+ it_behaves_like 'there is no bot token'
+ end
+
+ context 'when the 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_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id
+ }
+ )
+ is_expected.to be_error
+ end
+ end
+
+ context 'when the Slack API returns an error' do
+ let(:api_response) { { ok: false, foo: 'bar' } }
+
+ it 'tracks the exception and returns an error response' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Slack API returned an error'),
+ {
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ response: api_response.with_indifferent_access
+ }
+ )
+ is_expected.to be_error
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_events/url_verification_service_spec.rb b/spec/services/integrations/slack_events/url_verification_service_spec.rb
new file mode 100644
index 00000000000..0d668acafb9
--- /dev/null
+++ b/spec/services/integrations/slack_events/url_verification_service_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackEvents::UrlVerificationService, feature_category: :integrations do
+ describe '#execute' do
+ it 'returns the challenge' do
+ expect(described_class.new({ challenge: 'foo' }).execute).to eq({ challenge: 'foo' })
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interaction_service_spec.rb b/spec/services/integrations/slack_interaction_service_spec.rb
new file mode 100644
index 00000000000..599320c7986
--- /dev/null
+++ b/spec/services/integrations/slack_interaction_service_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractionService, feature_category: :integrations do
+ describe '#execute' do
+ subject(:execute) { described_class.new(params).execute }
+
+ let(:params) do
+ {
+ type: slack_interaction,
+ foo: 'bar'
+ }
+ end
+
+ 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, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ 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, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when block action service is submitted' do
+ let(:slack_interaction) { 'block_actions' }
+
+ it 'executes the block actions service' do
+ block_action_service = described_class::INTERACTIONS['block_actions']
+
+ expect_next_instance_of(block_action_service, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when slack_interaction is not known' do
+ let(:slack_interaction) { 'foo' }
+
+ it 'raises an error and does not execute a service class' do
+ described_class::INTERACTIONS.each_value do |service_class|
+ expect(service_class).not_to receive(:new)
+ end
+
+ expect { execute }.to raise_error(described_class::UnknownInteractionError)
+ end
+ end
+ end
+end
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
diff --git a/spec/services/integrations/slack_option_service_spec.rb b/spec/services/integrations/slack_option_service_spec.rb
new file mode 100644
index 00000000000..2e114b932d2
--- /dev/null
+++ b/spec/services/integrations/slack_option_service_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackOptionService, feature_category: :integrations do
+ describe '#execute' do
+ subject(:execute) { described_class.new(params).execute }
+
+ let_it_be(:slack_installation) { create(:slack_integration) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:chat_name) do
+ create(:chat_name,
+ user: user,
+ team_id: slack_installation.team_id,
+ chat_id: slack_installation.user_id
+ )
+ end
+
+ let(:params) do
+ {
+ action_id: action_id,
+ view: {
+ id: 'VHDFR54DSA'
+ },
+ value: 'Search value',
+ team: {
+ id: slack_installation.team_id
+ },
+ user: {
+ id: slack_installation.user_id
+ }
+ }
+ end
+
+ context 'when action_id is assignee' do
+ let(:action_id) { 'assignee' }
+
+ it 'executes the user search handler' do
+ user_search_handler = described_class::OPTIONS['assignee']
+
+ expect_next_instance_of(user_search_handler, chat_name, 'Search value', 'VHDFR54DSA') do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when action_id is labels' do
+ let(:action_id) { 'labels' }
+
+ it 'executes the label search handler' do
+ label_search_handler = described_class::OPTIONS['labels']
+
+ expect_next_instance_of(label_search_handler, chat_name, 'Search value', 'VHDFR54DSA') do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when action_id is unknown' do
+ let(:action_id) { 'foo' }
+
+ it 'raises an error and does not execute a service class' do
+ described_class::OPTIONS.each_value do |service_class|
+ expect(service_class).not_to receive(:new)
+ end
+
+ expect { execute }.to raise_error(described_class::UnknownOptionError)
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_options/label_search_handler_spec.rb b/spec/services/integrations/slack_options/label_search_handler_spec.rb
new file mode 100644
index 00000000000..3b006061f1d
--- /dev/null
+++ b/spec/services/integrations/slack_options/label_search_handler_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackOptions::LabelSearchHandler, feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, namespace: group) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:chat_name) { create(:chat_name, user: current_user) }
+ 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_it_be(:group_label1) { create(:group_label, group: group, title: 'LabelG 1') }
+ let_it_be(:group_label2) { create(:group_label, group: group, title: 'glb 2') }
+ let_it_be(:view_id) { 'VXHD54DR' }
+
+ let(:search_value) { 'Lab' }
+
+ subject(:execute) { described_class.new(chat_name, search_value, view_id).execute }
+
+ context 'when user has permission to read project and group labels' do
+ before do
+ allow(Rails.cache).to receive(:read).and_return(project.id)
+ project.add_developer(current_user)
+ end
+
+ it 'returns the labels matching the search term' do
+ labels = execute.payload[:options]
+ label_names = labels.map { |label| label.dig(:text, :text) }
+
+ expect(label_names).to contain_exactly(
+ project_label1.name,
+ project_label2.name,
+ group_label1.name
+ )
+ end
+ end
+
+ context 'when user does not have permissions to read project/group labels' do
+ it 'returns empty array' do
+ expect(LabelsFinder).not_to receive(:execute)
+
+ expect(execute.payload).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_options/user_search_handler_spec.rb b/spec/services/integrations/slack_options/user_search_handler_spec.rb
new file mode 100644
index 00000000000..e827bf643d2
--- /dev/null
+++ b/spec/services/integrations/slack_options/user_search_handler_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackOptions::UserSearchHandler, feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:chat_name) { create(:chat_name, user: current_user) }
+ let_it_be(:user1) { create(:user, name: 'Rajendra Kadam') }
+ let_it_be(:user2) { create(:user, name: 'Rajesh K') }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:view_id) { 'VXHD54DR' }
+
+ let(:search_value) { 'Raj' }
+
+ subject(:execute) { described_class.new(chat_name, search_value, view_id).execute }
+
+ context 'when user has permissions to read project members' do
+ before do
+ project.add_developer(current_user)
+ project.add_guest(user1)
+ project.add_reporter(user2)
+ project.add_maintainer(user3)
+ end
+
+ it 'returns the user matching the search term' do
+ expect(Rails.cache).to receive(:read).and_return(project.id)
+
+ members = execute.payload[:options]
+ user_names = members.map { |member| member.dig(:text, :text) }
+
+ expect(members.count).to eq(2)
+ expect(user_names).to contain_exactly(
+ "#{user1.name} - #{user1.username}",
+ "#{user2.name} - #{user2.username}"
+ )
+ end
+ end
+
+ context 'when user does not have permissions to read project members' do
+ it 'returns empty array' do
+ expect(Rails.cache).to receive(:read).and_return(project.id)
+ expect(MembersFinder).not_to receive(:execute)
+
+ members = execute.payload
+
+ expect(members).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/test/project_service_spec.rb b/spec/services/integrations/test/project_service_spec.rb
index 74833686283..4f8f932fb45 100644
--- a/spec/services/integrations/test/project_service_spec.rb
+++ b/spec/services/integrations/test/project_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Test::ProjectService do
+RSpec.describe Integrations::Test::ProjectService, feature_category: :integrations do
include AfterNextHelpers
describe '#execute' do