diff options
Diffstat (limited to 'spec/models')
-rw-r--r-- | spec/models/ci/build_spec.rb | 5 | ||||
-rw-r--r-- | spec/models/integrations/apple_app_store_spec.rb | 6 | ||||
-rw-r--r-- | spec/models/integrations/gitlab_slack_application_spec.rb | 337 | ||||
-rw-r--r-- | spec/models/integrations/slack_workspace/api_scope_spec.rb | 20 | ||||
-rw-r--r-- | spec/models/merge_request_spec.rb | 44 | ||||
-rw-r--r-- | spec/models/resource_milestone_event_spec.rb | 18 | ||||
-rw-r--r-- | spec/models/resource_state_event_spec.rb | 17 | ||||
-rw-r--r-- | spec/models/slack_integration_spec.rb | 147 | ||||
-rw-r--r-- | spec/models/user_spec.rb | 3 |
9 files changed, 553 insertions, 44 deletions
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f2c713c22a7..99326dfd02d 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3743,7 +3743,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def [ { key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: apple_app_store_integration.app_store_issuer_id, masked: true, public: false }, { key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(apple_app_store_integration.app_store_private_key), masked: true, public: false }, - { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: apple_app_store_integration.app_store_key_id, masked: true, public: false } + { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: apple_app_store_integration.app_store_key_id, masked: true, public: false }, + { key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64', value: "true", masked: false, public: false } ] end @@ -3769,6 +3770,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil + expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil end end end @@ -3778,6 +3780,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil + expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil end end end diff --git a/spec/models/integrations/apple_app_store_spec.rb b/spec/models/integrations/apple_app_store_spec.rb index b2a52c8aaf0..7487793cf4f 100644 --- a/spec/models/integrations/apple_app_store_spec.rb +++ b/spec/models/integrations/apple_app_store_spec.rb @@ -81,6 +81,12 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do value: apple_app_store_integration.app_store_key_id, masked: true, public: false + }, + { + key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64', + value: described_class::IS_KEY_CONTENT_BASE64, + masked: false, + public: false } ] diff --git a/spec/models/integrations/gitlab_slack_application_spec.rb b/spec/models/integrations/gitlab_slack_application_spec.rb new file mode 100644 index 00000000000..68476dde2a3 --- /dev/null +++ b/spec/models/integrations/gitlab_slack_application_spec.rb @@ -0,0 +1,337 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::GitlabSlackApplication, feature_category: :integrations do + include AfterNextHelpers + + it_behaves_like Integrations::BaseSlackNotification, factory: :gitlab_slack_application_integration do + before do + stub_request(:post, "#{::Slack::API::BASE_URL}/chat.postMessage").to_return(body: '{"ok":true}') + end + end + + describe 'validations' do + it { is_expected.not_to validate_presence_of(:webhook) } + end + + describe 'default values' do + it { expect(subject.category).to eq(:chat) } + + it { is_expected.not_to be_alert_events } + it { is_expected.not_to be_commit_events } + it { is_expected.not_to be_confidential_issues_events } + it { is_expected.not_to be_confidential_note_events } + it { is_expected.not_to be_deployment_events } + it { is_expected.not_to be_issues_events } + it { is_expected.not_to be_job_events } + it { is_expected.not_to be_merge_requests_events } + it { is_expected.not_to be_note_events } + it { is_expected.not_to be_pipeline_events } + it { is_expected.not_to be_push_events } + it { is_expected.not_to be_tag_push_events } + it { is_expected.not_to be_vulnerability_events } + it { is_expected.not_to be_wiki_page_events } + end + + describe '#execute' do + let_it_be(:user) { build_stubbed(:user) } + + let(:slack_integration) { build(:slack_integration) } + let(:data) { Gitlab::DataBuilder::Push.build_sample(integration.project, user) } + let(:slack_api_method_uri) { "#{::Slack::API::BASE_URL}/chat.postMessage" } + + let(:mock_message) do + instance_double(Integrations::ChatMessage::PushMessage, attachments: ['foo'], pretext: 'bar') + end + + subject(:integration) { build(:gitlab_slack_application_integration, slack_integration: slack_integration) } + + before do + allow(integration).to receive(:get_message).and_return(mock_message) + allow(integration).to receive(:log_usage) + end + + def stub_slack_request(channel: '#push_channel', success: true) + post_body = { + body: { + attachments: mock_message.attachments, + text: mock_message.pretext, + unfurl_links: false, + unfurl_media: false, + channel: channel + } + } + + response = { ok: success }.to_json + + stub_request(:post, slack_api_method_uri).with(post_body) + .to_return(body: response, headers: { 'Content-Type' => 'application/json; charset=utf-8' }) + end + + it 'notifies Slack' do + stub_slack_request + + expect(integration.execute(data)).to be true + end + + context 'when the integration is not configured for event' do + before do + integration.push_channel = nil + end + + it 'does not notify Slack' do + expect(integration.execute(data)).to be false + end + end + + context 'when Slack API responds with an error' do + it 'logs the error and API response' do + stub_slack_request(success: false) + + expect(Gitlab::IntegrationsLogger).to receive(:error).with( + { + integration_class: described_class.name, + integration_id: integration.id, + project_id: integration.project_id, + project_path: kind_of(String), + message: 'Slack API error when notifying', + api_response: { 'ok' => false } + } + ) + expect(integration.execute(data)).to be false + end + end + + context 'when there is an HTTP error' do + it 'logs the error' do + expect_next(Slack::API).to receive(:post).and_raise(Net::ReadTimeout) + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with( + kind_of(Net::ReadTimeout), + { + slack_integration_id: slack_integration.id, + integration_id: integration.id + } + ) + expect(integration.execute(data)).to be false + end + end + + context 'when configured to post to multiple Slack channels' do + before do + push_channels = '#first_channel, #second_channel' + integration.push_channel = push_channels + end + + it 'posts to both Slack channels and returns true' do + stub_slack_request(channel: '#first_channel') + stub_slack_request(channel: '#second_channel') + + expect(integration.execute(data)).to be true + end + + context 'when one of the posts responds with an error' do + it 'posts to both channels and returns true' do + stub_slack_request(channel: '#first_channel', success: false) + stub_slack_request(channel: '#second_channel') + + expect(Gitlab::IntegrationsLogger).to receive(:error).once + expect(integration.execute(data)).to be true + end + end + + context 'when both of the posts respond with an error' do + it 'posts to both channels and returns false' do + stub_slack_request(channel: '#first_channel', success: false) + stub_slack_request(channel: '#second_channel', success: false) + + expect(Gitlab::IntegrationsLogger).to receive(:error).twice + expect(integration.execute(data)).to be false + end + end + + context 'when one of the posts raises an HTTP exception' do + it 'posts to one channel and returns true' do + stub_slack_request(channel: '#second_channel') + + expect_next_instance_of(Slack::API) do |api_client| + expect(api_client).to receive(:post) + .with('chat.postMessage', hash_including(channel: '#first_channel')).and_raise(Net::ReadTimeout) + expect(api_client).to receive(:post) + .with('chat.postMessage', hash_including(channel: '#second_channel')).and_call_original + end + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).once + expect(integration.execute(data)).to be true + end + end + + context 'when both of the posts raise an HTTP exception' do + it 'posts to one channel and returns true' do + stub_slack_request(channel: '#second_channel') + + expect_next(Slack::API).to receive(:post).twice.and_raise(Net::ReadTimeout) + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).twice + expect(integration.execute(data)).to be false + end + end + end + end + + describe '#test' do + let(:integration) { build(:gitlab_slack_application_integration) } + + let(:slack_api_method_uri) { "#{::Slack::API::BASE_URL}/chat.postEphemeral" } + let(:response_failure) { { error: 'channel_not_found' } } + let(:response_success) { { error: 'user_not_in_channel' } } + let(:response_headers) { { 'Content-Type' => 'application/json; charset=utf-8' } } + let(:request_body) do + { + text: 'Test', + user: integration.bot_user_id + } + end + + subject(:result) { integration.test({}) } + + def stub_slack_request(channel:, success:) + response_body = success ? response_success : response_failure + + stub_request(:post, slack_api_method_uri) + .with(body: request_body.merge(channel: channel)) + .to_return(body: response_body.to_json, headers: response_headers) + end + + context 'when all channels can be posted to' do + before do + stub_slack_request(channel: anything, success: true) + end + + it 'is successful' do + is_expected.to eq({ success: true, result: nil }) + end + end + + context 'when the same channel is used for multiple events' do + let(:integration) do + build(:gitlab_slack_application_integration, all_channels: false, push_channel: '#foo', issue_channel: '#foo') + end + + it 'only tests the channel once' do + stub_slack_request(channel: '#foo', success: true) + + is_expected.to eq({ success: true, result: nil }) + expect(WebMock).to have_requested(:post, slack_api_method_uri).once + end + end + + context 'when there are channels that cannot be posted to' do + let(:unpostable_channels) { ['#push_channel', '#issue_channel'] } + + before do + stub_slack_request(channel: anything, success: true) + + unpostable_channels.each do |channel| + stub_slack_request(channel: channel, success: false) + end + end + + it 'returns an error message informing which channels cannot be posted to' do + expected_message = "Unable to post to #{unpostable_channels.to_sentence}, " \ + 'please add the GitLab Slack app to any private Slack channels' + + is_expected.to eq({ success: false, result: expected_message }) + end + + context 'when integration is not configured for notifications' do + let_it_be(:integration) { build(:gitlab_slack_application_integration, all_channels: false) } + + it 'is successful' do + is_expected.to eq({ success: true, result: nil }) + end + end + end + + context 'when integration is using legacy version of Slack app' do + before do + integration.slack_integration = build(:slack_integration, :legacy) + end + + it 'returns an error to inform the user to update their integration' do + expected_message = 'GitLab for Slack app must be reinstalled to enable notifications' + + is_expected.to eq({ success: false, result: expected_message }) + end + end + end + + context 'when the integration is active' do + before do + subject.active = true + end + + it 'is editable, and presents editable fields' do + expect(subject).to be_editable + expect(subject.fields).not_to be_empty + expect(subject.configurable_events).not_to be_empty + end + + it 'includes the expected sections' do + section_types = subject.sections.pluck(:type) + + expect(section_types).to eq( + [ + described_class::SECTION_TYPE_TRIGGER, + described_class::SECTION_TYPE_CONFIGURATION + ] + ) + end + end + + context 'when the integration is not active' do + before do + subject.active = false + end + + it 'is not editable, and presents no editable fields' do + expect(subject).not_to be_editable + expect(subject.fields).to be_empty + expect(subject.configurable_events).to be_empty + end + + it 'does not include sections' do + section_types = subject.sections.pluck(:type) + + expect(section_types).to be_empty + end + end + + describe '#description' do + specify { expect(subject.description).to be_present } + end + + describe '#upgrade_needed?' do + context 'with all_features_supported' do + subject(:integration) { create(:gitlab_slack_application_integration, :all_features_supported) } + + it 'is false' do + expect(integration).not_to be_upgrade_needed + end + end + + context 'without all_features_supported' do + subject(:integration) { create(:gitlab_slack_application_integration) } + + it 'is true' do + expect(integration).to be_upgrade_needed + end + end + + context 'without slack_integration' do + subject(:integration) { create(:gitlab_slack_application_integration, slack_integration: nil) } + + it 'is false' do + expect(integration).not_to be_upgrade_needed + end + end + end +end diff --git a/spec/models/integrations/slack_workspace/api_scope_spec.rb b/spec/models/integrations/slack_workspace/api_scope_spec.rb new file mode 100644 index 00000000000..92052983242 --- /dev/null +++ b/spec/models/integrations/slack_workspace/api_scope_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::SlackWorkspace::ApiScope, feature_category: :integrations do + describe '.find_or_initialize_by_names' do + it 'acts as insert into a global set of scope names' do + expect { described_class.find_or_initialize_by_names(%w[foo bar baz]) } + .to change { described_class.count }.by(3) + + expect { described_class.find_or_initialize_by_names(%w[bar baz foo buzz]) } + .to change { described_class.count }.by(1) + + expect { described_class.find_or_initialize_by_names(%w[baz foo]) } + .to change { described_class.count }.by(0) + + expect(described_class.pluck(:name)).to match_array(%w[foo bar baz buzz]) + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f3aa174a964..a66302da6f5 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -4082,12 +4082,18 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev ) end - context 'when service class is Ci::CompareCodequalityReportsService' do - let(:service_class) { 'Ci::CompareCodequalityReportsService' } + context 'when service class uses merge base pipeline' do + where(:service_class) do + %w[ + Ci::CompareMetricsReportsService + Ci::CompareCodequalityReportsService + Ci::CompareSecurityReportsService + ] + end context 'when merge request has a merge request pipeline' do let(:merge_request) do - create(:merge_request, :with_merge_request_pipeline) + create(:merge_request, :with_merge_request_pipeline, source_project: project) end let(:merge_base_pipeline) do @@ -4099,8 +4105,36 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev merge_request.update_head_pipeline end - it 'returns the merge_base_pipeline' do - expect(pipeline).to eq(merge_base_pipeline) + with_them do + it 'returns the merge_base_pipeline' do + expect(pipeline).to eq(merge_base_pipeline) + end + end + end + + context 'when merge does not have a merge request pipeline' do + with_them do + it 'returns the base_pipeline' do + expect(pipeline).to eq(base_pipeline) + end + end + end + end + + context 'when service class is Ci::CompareSecurityReportsService and feature flag is off' do + let(:service_class) { 'Ci::CompareSecurityReportsService' } + + before do + stub_feature_flags(use_merge_base_for_security_widget: false) + end + + context 'when merge request has a merge request pipeline' do + let(:merge_request) do + create(:merge_request, :with_merge_request_pipeline, source_project: project) + end + + it 'returns the base pipeline' do + expect(pipeline).to eq(base_pipeline) end end diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb index 80351862fc1..d237a16da8f 100644 --- a/spec/models/resource_milestone_event_spec.rb +++ b/spec/models/resource_milestone_event_spec.rb @@ -18,24 +18,6 @@ RSpec.describe ResourceMilestoneEvent, feature_category: :team_planning, type: : it { is_expected.to belong_to(:milestone) } end - describe 'scopes' do - describe '.aliased_for_timebox_report', :freeze_time do - let!(:event) { create(:resource_milestone_event, milestone: milestone) } - - let(:milestone) { create(:milestone) } - let(:scope) { described_class.aliased_for_timebox_report.first } - - it 'returns correct values with aliased names', :aggregate_failures do - expect(scope.event_type).to eq('timebox') - expect(scope.id).to eq(event.id) - expect(scope.issue_id).to eq(event.issue_id) - expect(scope.value).to eq(milestone.id) - expect(scope.action).to eq(event.action) - expect(scope.created_at).to eq(event.created_at) - end - end - end - describe '#milestone_title' do let(:milestone) { create(:milestone, title: 'v2.3') } let(:event) { create(:resource_milestone_event, milestone: milestone) } diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb index 699720b564a..a6d6b507b69 100644 --- a/spec/models/resource_state_event_spec.rb +++ b/spec/models/resource_state_event_spec.rb @@ -41,23 +41,6 @@ RSpec.describe ResourceStateEvent, feature_category: :team_planning, type: :mode end end - describe 'scopes' do - describe '.aliased_for_timebox_report', :freeze_time do - let!(:event) { create(:resource_state_event, issue: issue) } - - let(:scope) { described_class.aliased_for_timebox_report.first } - - it 'returns correct values with aliased names', :aggregate_failures do - expect(scope.event_type).to eq('state') - expect(scope.id).to eq(event.id) - expect(scope.issue_id).to eq(event.issue_id) - expect(scope.value).to eq(issue.state_id) - expect(scope.action).to eq(nil) - expect(scope.created_at).to eq(event.created_at) - end - end - end - context 'callbacks' do describe '#issue_usage_metrics' do describe 'when an issue is closed' do diff --git a/spec/models/slack_integration_spec.rb b/spec/models/slack_integration_spec.rb new file mode 100644 index 00000000000..41beeee598c --- /dev/null +++ b/spec/models/slack_integration_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SlackIntegration, feature_category: :integrations do + describe "Associations" do + it { is_expected.to belong_to(:integration) } + end + + describe 'authorized_scope_names' do + subject(:slack_integration) { create(:slack_integration) } + + it 'accepts assignment to nil' do + slack_integration.update!(authorized_scope_names: nil) + + expect(slack_integration.authorized_scope_names).to be_empty + end + + it 'accepts assignment to a string' do + slack_integration.update!(authorized_scope_names: 'foo') + + expect(slack_integration.authorized_scope_names).to contain_exactly('foo') + end + + it 'accepts assignment to an array of strings' do + slack_integration.update!(authorized_scope_names: %w[foo bar]) + + expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar') + end + + it 'accepts assignment to a comma-separated string' do + slack_integration.update!(authorized_scope_names: 'foo,bar') + + expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar') + end + + it 'strips white-space' do + slack_integration.update!(authorized_scope_names: 'foo , bar,baz') + + expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar', 'baz') + end + end + + describe 'all_features_supported?/upgrade_needed?' do + subject(:slack_integration) { create(:slack_integration) } + + context 'with enough scopes' do + before do + slack_integration.update!(authorized_scope_names: %w[chat:write.public chat:write commands]) + end + + it { is_expected.to be_all_features_supported } + it { is_expected.not_to be_upgrade_needed } + end + + %w[chat:write.public chat:write].each do |scope_name| + context "without #{scope_name}" do + before do + scopes = %w[chat:write.public chat:write] - [scope_name] + slack_integration.update!(authorized_scope_names: scopes) + end + + it { is_expected.not_to be_all_features_supported } + it { is_expected.to be_upgrade_needed } + end + end + end + + describe 'feature_available?' do + subject(:slack_integration) { create(:slack_integration) } + + context 'without any scopes' do + it 'is always true for :commands' do + expect(slack_integration).to be_feature_available(:commands) + end + + it 'is always false for others' do + expect(slack_integration).not_to be_feature_available(:notifications) + expect(slack_integration).not_to be_feature_available(:foo) + end + end + + context 'with enough scopes for notifications' do + before do + slack_integration.update!(authorized_scope_names: %w[chat:write.public chat:write foo]) + end + + it 'only has the correct features' do + expect(slack_integration).to be_feature_available(:commands) + expect(slack_integration).to be_feature_available(:notifications) + expect(slack_integration).not_to be_feature_available(:foo) + end + end + + context 'with enough scopes for commands' do + before do + slack_integration.update!(authorized_scope_names: %w[commands foo]) + end + + it 'only has the correct features' do + expect(slack_integration).to be_feature_available(:commands) + expect(slack_integration).not_to be_feature_available(:notifications) + expect(slack_integration).not_to be_feature_available(:foo) + end + end + + context 'with all scopes' do + before do + slack_integration.update!(authorized_scope_names: %w[commands chat:write chat:write.public]) + end + + it 'only has the correct features' do + expect(slack_integration).to be_feature_available(:commands) + expect(slack_integration).to be_feature_available(:notifications) + expect(slack_integration).not_to be_feature_available(:foo) + end + end + end + + describe 'Scopes' do + let_it_be(:slack_integration) { create(:slack_integration) } + let_it_be(:legacy_slack_integration) { create(:slack_integration, :legacy) } + + describe '#with_bot' do + it 'returns records with bot data' do + expect(described_class.with_bot).to contain_exactly(slack_integration) + end + end + + describe '#by_team' do + it 'returns records with shared team_id' do + team_id = slack_integration.team_id + team_slack_integration = create(:slack_integration, team_id: team_id) + + expect(described_class.by_team(team_id)).to contain_exactly(slack_integration, team_slack_integration) + end + end + end + + describe 'Validations' do + it { is_expected.to validate_presence_of(:team_id) } + it { is_expected.to validate_presence_of(:team_name) } + it { is_expected.to validate_presence_of(:alias) } + it { is_expected.to validate_presence_of(:user_id) } + it { is_expected.to validate_presence_of(:integration) } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bc677aca0f4..b36599b1273 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -105,9 +105,6 @@ RSpec.describe User, feature_category: :user_profile do it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil } - it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil } - it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil } - it { is_expected.to delegate_method(:discord).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:discord=).to(:user_detail).with_arguments(:args).allow_nil } |