Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/passwords_controller_spec.rb131
-rw-r--r--spec/factories/chat_names.rb2
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/access_spec.rb15
-rw-r--r--spec/lib/gitlab/slash_commands/verify_request_spec.rb36
-rw-r--r--spec/mailers/devise_mailer_spec.rb8
-rw-r--r--spec/migrations/sent_notifications_self_install_id_swap_spec.rb81
-rw-r--r--spec/models/concerns/recoverable_by_any_email_spec.rb131
-rw-r--r--spec/models/integrations/mattermost_slash_commands_spec.rb34
-rw-r--r--spec/models/integrations/slack_slash_commands_spec.rb23
-rw-r--r--spec/models/merge_request_spec.rb30
-rw-r--r--spec/requests/api/namespaces_spec.rb13
-rw-r--r--spec/requests/projects/integrations/slash_commands_controller_spec.rb139
-rw-r--r--spec/support/helpers/email_helpers.rb12
-rw-r--r--spec/support/helpers/user_with_namespace_shim.yml1
-rw-r--r--spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb72
15 files changed, 668 insertions, 60 deletions
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index aad946acad4..cff84da7382 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PasswordsController do
+RSpec.describe PasswordsController, feature_category: :system_access do
include DeviseHelpers
before do
@@ -109,8 +109,9 @@ RSpec.describe PasswordsController do
describe '#create' do
let(:user) { create(:user) }
+ let(:email) { user.email }
- subject(:perform_request) { post(:create, params: { user: { email: user.email } }) }
+ subject(:perform_request) { post(:create, params: { user: { email: email } }) }
context 'when reCAPTCHA is disabled' do
before do
@@ -161,5 +162,131 @@ RSpec.describe PasswordsController do
expect(flash[:notice]).to include 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.'
end
end
+
+ context "sending 'Reset password instructions' email" do
+ include EmailHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_confirmed_primary_email) { user.email }
+ let_it_be(:user_confirmed_secondary_email) { create(:email, :confirmed, user: user, email: 'confirmed-secondary-email@example.com').email }
+ let_it_be(:user_unconfirmed_secondary_email) { create(:email, user: user, email: 'unconfirmed-secondary-email@example.com').email }
+ let_it_be(:unknown_email) { 'attacker@example.com' }
+ let_it_be(:invalid_email) { 'invalid_email' }
+ let_it_be(:sql_injection_email) { 'sql-injection-email@example.com OR 1=1' }
+ let_it_be(:another_user_confirmed_primary_email) { create(:user).email }
+ let_it_be(:another_user_unconfirmed_primary_email) { create(:user, :unconfirmed).email }
+
+ before do
+ reset_delivered_emails!
+
+ perform_request
+
+ perform_enqueued_jobs
+ end
+
+ context "when email param matches user's confirmed primary email" do
+ let(:email) { user_confirmed_primary_email }
+
+ it 'sends email to the primary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [user_confirmed_primary_email])
+ end
+ end
+
+ context "when email param matches user's unconfirmed primary email" do
+ let(:email) { another_user_unconfirmed_primary_email }
+
+ # By default 'devise' gem allows password reset by unconfirmed primary email.
+ # When user account with unconfirmed primary email that means it is unconfirmed.
+ #
+ # Password reset by unconfirmed primary email is very helpful from
+ # security perspective. Example:
+ # Malicious person creates user account on GitLab with someone's email.
+ # If the email owner confirms the email for newly created account, the malicious person will be able
+ # to sign in into the account by password they provided during account signup.
+ # The malicious person could set up 2FA to the user account, after that
+ # te email owner would not able to get access to that user account even
+ # after performing password reset.
+ # To deal with that case safely the email owner should reset password
+ # for the user account first. That will make sure that after the user account
+ # is confirmed the malicious person is not be able to sign in with
+ # the password they provided during the account signup. Then email owner
+ # could sign into the account, they will see a prompt to confirm the account email
+ # to proceed. They can safely confirm the email and take over the account.
+ # That is one of the reasons why password reset by unconfirmed primary email should be allowed.
+ it 'sends email to the primary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [another_user_unconfirmed_primary_email])
+ end
+ end
+
+ context "when email param matches user's confirmed secondary email" do
+ let(:email) { user_confirmed_secondary_email }
+
+ it 'sends email to the confirmed secondary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [user_confirmed_secondary_email])
+ end
+ end
+
+ # While unconfirmed primary emails are linked with users accounts,
+ # unconfirmed secondary emails should not be linked with any users till they are confirmed
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/356665
+ #
+ # In https://gitlab.com/gitlab-org/gitlab/-/issues/367823, it is considerd
+ # to prevent reserving emails on Gitlab by unconfirmed secondary emails.
+ # As per this issue, there might be cases that there are multiple users
+ # with the same unconfirmed secondary emails. It would be impossible to identify for
+ # what user account password reset is requested if password reset were allowed
+ # by unconfirmed secondary emails.
+ # Also note that it is not possible to request email confirmation for
+ # unconfirmed secondary emails without having access to the user account.
+ context "when email param matches user's unconfirmed secondary email" do
+ let(:email) { user_unconfirmed_secondary_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param is unknown email' do
+ let(:email) { unknown_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param is invalid email' do
+ let(:email) { invalid_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param with attempt to cause SQL injection' do
+ let(:email) { sql_injection_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/436084
+ context 'when email param with multiple emails' do
+ let(:email) do
+ [
+ user_confirmed_primary_email,
+ user_confirmed_secondary_email,
+ user_unconfirmed_secondary_email,
+ unknown_email,
+ another_user_confirmed_primary_email,
+ another_user_unconfirmed_primary_email
+ ]
+ end
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+ end
end
end
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
index c872694ee64..ee29f0fa0e4 100644
--- a/spec/factories/chat_names.rb
+++ b/spec/factories/chat_names.rb
@@ -7,6 +7,8 @@ FactoryBot.define do
team_id { 'T0001' }
team_domain { 'Awesome Team' }
+ token { SecureRandom.uuid }
+
sequence(:chat_id) { |n| "U#{n}" }
chat_name { generate(:username) }
end
diff --git a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
index 3af0ae03256..d83871d67a2 100644
--- a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
+++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
@@ -76,4 +76,19 @@ RSpec.describe Gitlab::SlashCommands::Presenters::Access do
end
end
end
+
+ describe '#confirm' do
+ let(:url) { 'https://example.com/api' }
+
+ subject { described_class.new.confirm(url) }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to confirm the request' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match(
+ "Please confirm the request by accessing <#{url}|this link> through a web browser"
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/slash_commands/verify_request_spec.rb b/spec/lib/gitlab/slash_commands/verify_request_spec.rb
new file mode 100644
index 00000000000..df6052ded93
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/verify_request_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SlashCommands::VerifyRequest, feature_category: :integrations do
+ let_it_be(:integration) { create(:slack_slash_commands_integration) }
+ let_it_be(:chat_name) { create(:chat_name) }
+ let(:response_url) { 'http://www.example.com/' }
+
+ subject(:verification) { described_class.new(integration, chat_name, response_url) }
+
+ describe '#approve!' do
+ before do
+ stub_request(:post, "http://www.example.com/").to_return(status: 200, body: 'ok')
+ end
+
+ it 'updates the token' do
+ expect { verification.approve! }.to change { chat_name.reload.token }.to(integration.token)
+ end
+
+ it 'updates the ephemeral message' do
+ expect(Gitlab::HTTP).to receive(:post).with(
+ response_url, a_hash_including(body: an_instance_of(String), headers: an_instance_of(Hash))
+ ).once
+
+ verification.approve!
+ end
+ end
+
+ describe '#valid?' do
+ it 'compares tokens' do
+ expect(ActiveSupport::SecurityUtils).to receive(:secure_compare).with(integration.token, chat_name.token)
+ verification.valid?
+ end
+ end
+end
diff --git a/spec/mailers/devise_mailer_spec.rb b/spec/mailers/devise_mailer_spec.rb
index 0a6d38996b7..af3a9457527 100644
--- a/spec/mailers/devise_mailer_spec.rb
+++ b/spec/mailers/devise_mailer_spec.rb
@@ -103,10 +103,10 @@ RSpec.describe DeviseMailer, feature_category: :user_management do
describe '#reset_password_instructions' do
let_it_be(:user) { create(:user) }
- let(:params) { {} }
+ let(:opts) { {} }
subject do
- described_class.reset_password_instructions(user, 'faketoken', params)
+ described_class.reset_password_instructions(user, 'faketoken', opts)
end
it_behaves_like 'an email sent from GitLab'
@@ -139,9 +139,9 @@ RSpec.describe DeviseMailer, feature_category: :user_management do
is_expected.to have_header 'X-Mailgun-Suppressions-Bypass', 'true'
end
- context 'with email in params' do
+ context 'with email in opts' do
let(:email) { 'example@example.com' }
- let(:params) { { to: email } }
+ let(:opts) { { to: email } }
it 'is sent to the specified email' do
is_expected.to deliver_to email
diff --git a/spec/migrations/sent_notifications_self_install_id_swap_spec.rb b/spec/migrations/sent_notifications_self_install_id_swap_spec.rb
new file mode 100644
index 00000000000..db66b72d2ec
--- /dev/null
+++ b/spec/migrations/sent_notifications_self_install_id_swap_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SentNotificationsSelfInstallIdSwap, feature_category: :database do
+ let(:connection) { described_class.new.connection }
+
+ describe '#up' do
+ before do
+ # rubocop: disable RSpec/AnyInstanceOf -- This mixin is only used for migrations, it's okay to use this
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(dot_com?)
+ # rubocop: enable RSpec/AnyInstanceOf
+ end
+
+ context 'when we are NOT GitLab.com, dev, or test' do
+ let(:dot_com?) { false }
+
+ context 'when sent_notifications.id is not a bigint' do
+ around do |example|
+ connection.execute('ALTER TABLE sent_notifications ALTER COLUMN id TYPE integer')
+ example.run
+ connection.execute('ALTER TABLE sent_notifications ALTER COLUMN id TYPE bigint')
+ end
+
+ context 'when id_convert_to_bigint exists' do
+ around do |example|
+ connection.execute('ALTER TABLE sent_notifications ADD COLUMN IF NOT EXISTS id_convert_to_bigint bigint')
+ Gitlab::Database::UnidirectionalCopyTrigger.on_table(:sent_notifications, connection: connection).create(
+ :id, :id_convert_to_bigint)
+ example.run
+ connection.execute('ALTER TABLE sent_notifications DROP COLUMN id_convert_to_bigint')
+ end
+
+ it 'swaps the integer and bigint columns' do
+ sent_notifications = table(:sent_notifications)
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ sent_notifications.reset_column_information
+ expect(sent_notifications.columns.find { |c| c.name == 'id' }.sql_type).to eq('integer')
+ expect(sent_notifications.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ sent_notifications.reset_column_information
+ expect(sent_notifications.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ expect(sent_notifications.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('integer')
+ }
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context 'when any other condition' do
+ let(:dot_com?) { true }
+
+ it 'does not do anything' do
+ sent_notifications = table(:sent_notifications)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(sent_notifications.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ expect(sent_notifications.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/recoverable_by_any_email_spec.rb b/spec/models/concerns/recoverable_by_any_email_spec.rb
index 1e701f145be..ba0bb99effb 100644
--- a/spec/models/concerns/recoverable_by_any_email_spec.rb
+++ b/spec/models/concerns/recoverable_by_any_email_spec.rb
@@ -4,79 +4,140 @@ require 'spec_helper'
RSpec.describe RecoverableByAnyEmail, feature_category: :system_access do
describe '.send_reset_password_instructions' do
- let_it_be(:user) { create(:user, email: 'test@example.com') }
- let_it_be(:verified_email) { create(:email, :confirmed, user: user) }
- let_it_be(:unverified_email) { create(:email, user: user) }
+ include EmailHelpers
subject(:send_reset_password_instructions) do
User.send_reset_password_instructions(email: email)
end
- shared_examples 'sends the password reset email' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_confirmed_primary_email) { user.email }
+
+ let_it_be(:user_confirmed_secondary_email) do
+ create(:email, :confirmed, user: user, email: 'confirmed-secondary-email@example.com').email
+ end
+
+ let_it_be(:user_unconfirmed_secondary_email) do
+ create(:email, user: user, email: 'unconfirmed-secondary-email@example.com').email
+ end
+
+ let_it_be(:unknown_email) { 'attacker@example.com' }
+ let_it_be(:invalid_email) { 'invalid_email' }
+ let_it_be(:sql_injection_email) { 'sql-injection-email@example.com OR 1=1' }
+
+ let_it_be(:another_user_confirmed_primary_email) { create(:user).email }
+
+ let_it_be(:another_user) { create(:user, :unconfirmed) }
+ let_it_be(:another_user_unconfirmed_primary_email) { another_user.email }
+
+ shared_examples "sends 'Reset password instructions' email" do
it 'finds the user' do
- expect(send_reset_password_instructions).to eq(user)
+ expect(send_reset_password_instructions).to eq(expected_user)
end
it 'sends the email' do
+ reset_delivered_emails!
+
expect { send_reset_password_instructions }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+
+ perform_enqueued_jobs
+
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [email])
end
end
- shared_examples 'does not send the password reset email' do
+ shared_examples "does not send 'Reset password instructions' email" do
+ # If user is not found, returns a new user with errors.
+ # See https://github.com/heartcombo/devise/blob/main/lib/devise/models/recoverable.rb
it 'does not find the user' do
- expect(subject.id).to be_nil
- expect(subject.errors).not_to be_empty
+ expect(send_reset_password_instructions).to be_instance_of User
+ expect(send_reset_password_instructions).to be_new_record
+ expect(send_reset_password_instructions.errors).not_to be_empty
end
- it 'does not send any email' do
- subject
+ it 'does not send email to anyone' do
+ reset_delivered_emails!
+
+ expect { send_reset_password_instructions }
+ .not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+
+ perform_enqueued_jobs
- expect { subject }.not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+ should_not_email_anyone
end
end
- context 'with user primary email' do
- let(:email) { user.email }
+ context "when email param matches user's confirmed primary email" do
+ let(:expected_user) { user }
+ let(:email) { user_confirmed_primary_email }
- it_behaves_like 'sends the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- context 'with user verified email' do
- let(:email) { verified_email.email }
+ context "when email param matches user's unconfirmed primary email" do
+ let(:expected_user) { another_user }
+ let(:email) { another_user_unconfirmed_primary_email }
- it_behaves_like 'sends the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- context 'with user unverified email' do
- let(:email) { unverified_email.email }
+ context "when email param matches user's confirmed secondary email" do
+ let(:expected_user) { user }
+ let(:email) { user_confirmed_secondary_email }
- it_behaves_like 'does not send the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- end
- describe '#send_reset_password_instructions' do
- let_it_be(:user) { create(:user) }
- let_it_be(:opts) { { email: 'random@email.com' } }
- let_it_be(:token) { 'passwordresettoken' }
+ context "when email param matches user's unconfirmed secondary email" do
+ let(:email) { user_unconfirmed_secondary_email }
- before do
- allow(user).to receive(:set_reset_password_token).and_return(token)
+ it_behaves_like "does not send 'Reset password instructions' email"
end
- subject { user.send_reset_password_instructions(opts) }
+ context 'when email param is unknown email' do
+ let(:email) { unknown_email }
- it 'sends the email' do
- expect { subject }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+ it_behaves_like "does not send 'Reset password instructions' email"
end
- it 'calls send_reset_password_instructions_notification with correct arguments' do
- expect(user).to receive(:send_reset_password_instructions_notification).with(token, opts)
+ context 'when email param is invalid email' do
+ let(:email) { invalid_email }
- subject
+ it_behaves_like "does not send 'Reset password instructions' email"
end
- it 'returns the generated token' do
- expect(subject).to eq(token)
+ context 'when email param with attempt to cause SQL injection' do
+ let(:email) { sql_injection_email }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is nil' do
+ let(:email) { nil }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is empty string' do
+ let(:email) { '' }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/436084
+ context 'when email param with multiple emails' do
+ let(:email) do
+ [
+ user_confirmed_primary_email,
+ user_confirmed_secondary_email,
+ user_unconfirmed_secondary_email,
+ unknown_email,
+ another_user_confirmed_primary_email,
+ another_user_unconfirmed_primary_email
+ ]
+ end
+
+ it_behaves_like "does not send 'Reset password instructions' email"
end
end
end
diff --git a/spec/models/integrations/mattermost_slash_commands_spec.rb b/spec/models/integrations/mattermost_slash_commands_spec.rb
index 3dee8737067..43316e164ed 100644
--- a/spec/models/integrations/mattermost_slash_commands_spec.rb
+++ b/spec/models/integrations/mattermost_slash_commands_spec.rb
@@ -125,5 +125,39 @@ RSpec.describe Integrations::MattermostSlashCommands, feature_category: :integra
end
end
end
+
+ describe '#redirect_url' do
+ let(:url) { 'http://www.mattermost.com/hooks' }
+
+ subject { integration.redirect_url('team', 'channel', url) }
+
+ it { is_expected.to eq("http://www.mattermost.com/team/channels/channel") }
+
+ context 'with invalid URL scheme' do
+ let(:url) { 'javascript://www.mattermost.com/hooks' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with unsafe URL' do
+ let(:url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#confirmation_url' do
+ let(:params) do
+ {
+ team_domain: 'gitlab',
+ channel_name: 'test-channel',
+ response_url: 'http://mattermost.gitlab.com/hooks/commands/my123command'
+ }
+ end
+
+ subject { integration.confirmation_url('command-id', params) }
+
+ it { is_expected.to be_present }
+ end
end
end
diff --git a/spec/models/integrations/slack_slash_commands_spec.rb b/spec/models/integrations/slack_slash_commands_spec.rb
index f373fc2a2de..5d6f214a5d5 100644
--- a/spec/models/integrations/slack_slash_commands_spec.rb
+++ b/spec/models/integrations/slack_slash_commands_spec.rb
@@ -40,4 +40,27 @@ RSpec.describe Integrations::SlackSlashCommands, feature_category: :integrations
end
end
end
+
+ describe '#redirect_url' do
+ let(:integration) { build(:slack_slash_commands_integration) }
+
+ subject { integration.redirect_url('team', 'channel', 'www.example.com') }
+
+ it { is_expected.to eq('slack://channel?team=team&id=channel') }
+ end
+
+ describe '#confirmation_url' do
+ let(:integration) { build(:slack_slash_commands_integration) }
+ let(:params) do
+ {
+ team_id: 'T123456',
+ channel_id: 'C654321',
+ response_url: 'https://hooks.slack.com/services/T123456/C654321'
+ }
+ end
+
+ subject { integration.confirmation_url('command-id', params) }
+
+ it { is_expected.to be_present }
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c30bbb3a134..797ab5be235 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -6181,4 +6181,34 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { is_expected.to eq(false) }
end
+
+ describe '#previous_diff' do
+ let(:merge_request) { create(:merge_request, :skip_diff_creation) }
+
+ subject { merge_request.previous_diff }
+
+ context 'when there is are no merge_request_diffs' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is one merge request_diff' do
+ let(:merge_request) { create(:merge_request) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there are multiple merge_request_diffs' do
+ let(:oldest_merge_request_diff) { create(:merge_request_diff, merge_request: merge_request) }
+ let(:second_to_last_merge_request_diff) { create(:merge_request_diff, merge_request: merge_request) }
+ let(:most_recent_merge_request_diff) { create(:merge_request_diff, merge_request: merge_request) }
+
+ before do
+ oldest_merge_request_diff
+ second_to_last_merge_request_diff
+ most_recent_merge_request_diff
+ end
+
+ it { is_expected.to eq(second_to_last_merge_request_diff) }
+ end
+ end
end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 5fd41013b25..2320b3be0c1 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -109,6 +109,19 @@ RSpec.describe API::Namespaces, :aggregate_failures, feature_category: :groups_a
expect(json_response.map { |resource| resource['id'] }).to match_array([user.namespace_id, group2.id])
end
end
+
+ context 'with top_level_only param' do
+ it 'returns only top level groups' do
+ group1.add_owner(user)
+ group2.add_owner(user)
+
+ get api("/namespaces?top_level_only=true", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.map { |resource| resource['id'] }).to match_array([user.namespace_id, group1.id])
+ end
+ end
end
end
diff --git a/spec/requests/projects/integrations/slash_commands_controller_spec.rb b/spec/requests/projects/integrations/slash_commands_controller_spec.rb
new file mode 100644
index 00000000000..3d61f882bdf
--- /dev/null
+++ b/spec/requests/projects/integrations/slash_commands_controller_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Projects::Integrations::SlashCommandsController, feature_category: :integrations do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+ let_it_be(:chat_name) { create(:chat_name, user: user) }
+
+ let(:params) do
+ {
+ command_id: 'command-id',
+ integration: 'mattermost_slash_commands',
+ team: 1,
+ channel: 2,
+ response_url: 'http://www.example.com'
+ }
+ end
+
+ before do
+ create(:mattermost_slash_commands_integration, project: project)
+ end
+
+ describe 'GET #show' do
+ context 'when user is signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when request is invalid' do
+ it 'renders the "show" template with expired message' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include(
+ 'The slash command verification request has expired. Please run the command again.'
+ )
+ end
+ end
+
+ context 'when request is valid', :use_clean_rails_memory_store_caching do
+ before do
+ Rails.cache.write(
+ "slash-command-requests:#{params[:command_id]}", { team_id: chat_name.team_id, user_id: chat_name.chat_id }
+ )
+ stub_request(:post, "http://www.example.com/").to_return(status: 200, body: 'ok')
+ end
+
+ context 'when user is valid' do
+ it 'renders the "show" template with authorize button' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('Authorize')
+ end
+ end
+
+ context 'when user is invalid' do
+ let(:chat_name) { create(:chat_name) }
+
+ it 'renders the "show" template' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('The slash command request is invalid.')
+ end
+ end
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects with a status of 302' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+
+ describe 'POST #confirm' do
+ let(:params) { super().merge(redirect_url: 'http://www.example.com') }
+
+ context 'when user is signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when request is invalid' do
+ it 'renders the "show" template' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('The slash command request is invalid.')
+ end
+ end
+
+ context 'when request is valid', :use_clean_rails_memory_store_caching do
+ before do
+ Rails.cache.write(
+ "slash-command-requests:#{params[:command_id]}", { team_id: chat_name.team_id, user_id: chat_name.chat_id }
+ )
+ stub_request(:post, "http://www.example.com/").to_return(status: 200, body: 'ok')
+ end
+
+ context 'when user is valid' do
+ it 'redirects back to the integration' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+
+ context 'when user is invalid' do
+ let(:chat_name) { create(:chat_name) }
+
+ it 'renders the "show" template' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('The slash command request is invalid.')
+ end
+ end
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects with a status of 302' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 3583889d305..5d06eb64e0b 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -14,6 +14,18 @@ module EmailHelpers
ActiveJob::Base.queue_adapter.enqueued_jobs.clear
end
+ def expect_only_one_email_to_be_sent(subject:, to:)
+ count_of_sent_emails = ActionMailer::Base.deliveries.count
+ expect(count_of_sent_emails).to eq(1), "Expected only one email to be sent, but #{count_of_sent_emails} emails were sent instead"
+
+ return unless count_of_sent_emails == 1
+
+ message = ActionMailer::Base.deliveries.first
+
+ expect(message.subject).to eq(subject), "Expected '#{subject}' email to be sent, but '#{message.subject}' email was sent instead"
+ expect(message.to).to match_array(to), "Expected the email to be sent to #{to}, but it was sent to #{message.to} instead"
+ end
+
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)
diff --git a/spec/support/helpers/user_with_namespace_shim.yml b/spec/support/helpers/user_with_namespace_shim.yml
index 3e5556b6d58..7b3dc099cf9 100644
--- a/spec/support/helpers/user_with_namespace_shim.yml
+++ b/spec/support/helpers/user_with_namespace_shim.yml
@@ -43,6 +43,7 @@
- ee/spec/features/issues/user_sees_empty_state_spec.rb
- ee/spec/features/issues/user_uses_quick_actions_spec.rb
- ee/spec/features/issues/user_views_issues_spec.rb
+- ee/spec/features/merge_request/code_owner_approvals_reset_after_merging_to_source_branch_spec.rb
- ee/spec/features/merge_request/draft_comments_spec.rb
- ee/spec/features/merge_request/user_approves_with_password_spec.rb
- ee/spec/features/merge_request/user_approves_with_saml_auth_spec.rb
diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
index ff3cc1841b4..90bb75d402f 100644
--- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
@@ -91,38 +91,72 @@ RSpec.shared_examples Integrations::BaseSlashCommands do
described_class.create!(project: project, properties: { token: 'token' })
end
- it 'triggers the command' do
- expect_any_instance_of(Gitlab::SlashCommands::Command).to receive(:execute)
+ context 'with verified request' do
+ before do
+ allow_next_instance_of(::Gitlab::SlashCommands::VerifyRequest) do |instance|
+ allow(instance).to receive(:valid?).and_return(true)
+ end
+ end
- subject.trigger(params)
- end
+ it 'triggers the command' do
+ expect_any_instance_of(Gitlab::SlashCommands::Command).to receive(:execute)
+
+ subject.trigger(params)
+ end
- shared_examples_for 'blocks command execution' do
- it do
- expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
+ shared_examples_for 'blocks command execution' do
+ it do
+ expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
- result = subject.trigger(params)
- expect(result[:text]).to match(error_message)
+ result = subject.trigger(params)
+ expect(result[:text]).to match(error_message)
+ end
end
- end
- context 'when user is blocked' do
- before do
- chat_name.user.block
+ context 'when user is blocked' do
+ before do
+ chat_name.user.block
+ end
+
+ it_behaves_like 'blocks command execution' do
+ let(:error_message) { 'you do not have access to the GitLab project' }
+ end
end
- it_behaves_like 'blocks command execution' do
- let(:error_message) { 'you do not have access to the GitLab project' }
+ context 'when user is deactivated' do
+ before do
+ chat_name.user.deactivate
+ end
+
+ it_behaves_like 'blocks command execution' do
+ let(:error_message) { "your #{Gitlab.config.gitlab.url} account needs to be reactivated" }
+ end
end
end
- context 'when user is deactivated' do
+ context 'with unverified request' do
before do
- chat_name.user.deactivate
+ allow_next_instance_of(::Gitlab::SlashCommands::VerifyRequest) do |instance|
+ allow(instance).to receive(:valid?).and_return(false)
+ end
end
- it_behaves_like 'blocks command execution' do
- let(:error_message) { "your #{Gitlab.config.gitlab.url} account needs to be reactivated" }
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ channel_name: 'channel-test',
+ team_id: chat_name.team_id,
+ user_id: chat_name.chat_id,
+ response_url: 'http://domain.tld/commands',
+ token: 'token'
+ }
+ end
+
+ it 'caches the slash command params and returns confirmation message' do
+ expect(Rails.cache).to receive(:write).with(an_instance_of(String), params, { expires_in: 3.minutes })
+ expect_any_instance_of(Gitlab::SlashCommands::Presenters::Access).to receive(:confirm)
+
+ subject.trigger(params)
end
end
end