diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-30 21:11:31 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-30 21:11:31 +0300 |
commit | c753fd0bf4a5cc09f69941daef0f6fe99d61f20e (patch) | |
tree | 9aee7f1af879446f226d7a67c149c817ace3f69f /spec | |
parent | eaec42f9e37fe51f9c53fa7079639ec9f4c40efc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
66 files changed, 1963 insertions, 759 deletions
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb index 744c0712d6b..ccf4454c349 100644 --- a/spec/controllers/admin/impersonations_controller_spec.rb +++ b/spec/controllers/admin/impersonations_controller_spec.rb @@ -92,6 +92,14 @@ RSpec.describe Admin::ImpersonationsController do expect(warden.user).to eq(impersonator) end + + it 'clears token session keys' do + session[:bitbucket_token] = SecureRandom.hex(8) + + delete :destroy + + expect(session[:bitbucket_token]).to be_nil + end end # base case diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 015c36c9335..3a2b5dcb99d 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -794,6 +794,14 @@ RSpec.describe Admin::UsersController do expect(flash[:alert]).to eq("You are now impersonating #{user.username}") end + + it 'clears token session keys' do + session[:github_access_token] = SecureRandom.hex(8) + + post :impersonate, params: { id: user.username } + + expect(session[:github_access_token]).to be_nil + end end context "when impersonation is disabled" do @@ -807,5 +815,20 @@ RSpec.describe Admin::UsersController do expect(response).to have_gitlab_http_status(:not_found) end end + + context 'when impersonating an admin and attempting to impersonate again' do + let(:admin2) { create(:admin) } + + before do + post :impersonate, params: { id: admin2.username } + end + + it 'does not allow double impersonation', :aggregate_failures do + post :impersonate, params: { id: user.username } + + expect(flash[:alert]).to eq(_('You are already impersonating another user')) + expect(warden.user).to eq(admin2) + end + end end end diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 3e4b159271a..568712d29cb 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -54,6 +54,48 @@ RSpec.describe Import::GiteaController do end end end + + context 'when DNS Rebinding protection is enabled' do + let(:token) { 'gitea token' } + + let(:ip_uri) { 'http://167.99.148.217' } + let(:uri) { 'try.gitea.io' } + let(:https_uri) { "https://#{uri}" } + let(:http_uri) { "http://#{uri}" } + + before do + session[:gitea_access_token] = token + + allow(Gitlab::UrlBlocker).to receive(:validate!).with(https_uri, anything).and_return([Addressable::URI.parse(https_uri), uri]) + allow(Gitlab::UrlBlocker).to receive(:validate!).with(http_uri, anything).and_return([Addressable::URI.parse(ip_uri), uri]) + + allow(Gitlab::LegacyGithubImport::Client).to receive(:new).and_return(double('Gitlab::LegacyGithubImport::Client', repos: [], orgs: [])) + end + + context 'when provided host url is using https' do + let(:host_url) { https_uri } + + it 'uses unchanged host url to send request to Gitea' do + expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: https_uri, api_version: 'v1', hostname: 'try.gitea.io') + + get :status, format: :json + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when provided host url is using http' do + let(:host_url) { http_uri } + + it 'uses changed host url to send request to Gitea' do + expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(token, host: 'http://167.99.148.217', api_version: 'v1', hostname: 'try.gitea.io') + + get :status, format: :json + + expect(response).to have_gitlab_http_status(:ok) + end + end + end end end diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index f21ef324884..5bf3b4c48bf 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -98,6 +98,19 @@ RSpec.describe Oauth::ApplicationsController do end describe 'POST #create' do + let(:oauth_params) do + { + doorkeeper_application: { + name: 'foo', + redirect_uri: redirect_uri, + scopes: scopes + } + } + end + + let(:redirect_uri) { 'http://example.org' } + let(:scopes) { ['api'] } + subject { post :create, params: oauth_params } it 'creates an application' do @@ -116,38 +129,42 @@ RSpec.describe Oauth::ApplicationsController do expect(response).to redirect_to(profile_path) end - context 'redirect_uri' do + context 'when redirect_uri is invalid' do + let(:redirect_uri) { 'javascript://alert()' } + render_views it 'shows an error for a forbidden URI' do - invalid_uri_params = { - doorkeeper_application: { - name: 'foo', - redirect_uri: 'javascript://alert()', - scopes: ['api'] - } - } - - post :create, params: invalid_uri_params + subject expect(response.body).to include 'Redirect URI is forbidden by the server' + expect(response).to render_template('doorkeeper/applications/index') end end context 'when scopes are not present' do + let(:scopes) { [] } + render_views it 'shows an error for blank scopes' do - invalid_uri_params = { - doorkeeper_application: { - name: 'foo', - redirect_uri: 'http://example.org' - } - } - - post :create, params: invalid_uri_params + subject expect(response.body).to include 'Scopes can't be blank' + expect(response).to render_template('doorkeeper/applications/index') + end + end + + context 'when scopes are invalid' do + let(:scopes) { %w(api foo) } + + render_views + + it 'shows an error for invalid scopes' do + subject + + expect(response.body).to include 'Scopes doesn't match configured on the server.' + expect(response).to render_template('doorkeeper/applications/index') end end @@ -185,14 +202,4 @@ RSpec.describe Oauth::ApplicationsController do def disable_user_oauth allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false) end - - def oauth_params - { - doorkeeper_application: { - name: 'foo', - redirect_uri: 'http://example.org', - scopes: ['api'] - } - } - end end diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 073180cbafd..a0e2cf671af 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -35,6 +35,27 @@ RSpec.describe Profiles::TwoFactorAuthsController do end end + shared_examples 'user must enter a valid current password' do + let(:current_password) { '123' } + + it 'requires the current password', :aggregate_failures do + go + + expect(response).to redirect_to(profile_two_factor_auth_path) + expect(flash[:alert]).to eq(_('You must provide a valid current password')) + end + + context 'when the user is on the last sign in attempt' do + it do + user.update!(failed_attempts: User.maximum_attempts.pred) + + go + + expect(user.reload).to be_access_locked + end + end + end + describe 'GET show' do let_it_be_with_reload(:user) { create(:user) } @@ -69,9 +90,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do let_it_be_with_reload(:user) { create(:user) } let(:pin) { 'pin-code' } + let(:current_password) { user.password } def go - post :create, params: { pin_code: pin } + post :create, params: { pin_code: pin, current_password: current_password } end context 'with valid pin' do @@ -136,21 +158,25 @@ RSpec.describe Profiles::TwoFactorAuthsController do end end + it_behaves_like 'user must enter a valid current password' + it_behaves_like 'user must first verify their primary email address' end describe 'POST codes' do let_it_be_with_reload(:user) { create(:user, :two_factor) } + let(:current_password) { user.password } + it 'presents plaintext codes for the user to save' do expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c)) - post :codes + post :codes, params: { current_password: current_password } expect(assigns[:codes]).to match_array %w(a b c) end it 'persists the generated codes' do - post :codes + post :codes, params: { current_password: current_password } user.reload expect(user.otp_backup_codes).not_to be_empty @@ -159,12 +185,18 @@ RSpec.describe Profiles::TwoFactorAuthsController do it 'dismisses the `TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK` callout' do expect(controller.helpers).to receive(:dismiss_two_factor_auth_recovery_settings_check) - post :codes + post :codes, params: { current_password: current_password } + end + + it_behaves_like 'user must enter a valid current password' do + let(:go) { post :codes, params: { current_password: current_password } } end end describe 'DELETE destroy' do - subject { delete :destroy } + subject { delete :destroy, params: { current_password: current_password } } + + let(:current_password) { user.password } context 'for a user that has 2FA enabled' do let_it_be_with_reload(:user) { create(:user, :two_factor) } @@ -187,6 +219,10 @@ RSpec.describe Profiles::TwoFactorAuthsController do expect(flash[:notice]) .to eq _('Two-factor authentication has been disabled successfully!') end + + it_behaves_like 'user must enter a valid current password' do + let(:go) { delete :destroy, params: { current_password: current_password } } + end end context 'for a user that does not have 2FA enabled' do diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index be5c1f0d428..c352524ec14 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -624,9 +624,9 @@ RSpec.describe Projects::ProjectMembersController do end end - context 'when user can access source project members' do + context 'when user can admin source project members' do before do - another_project.add_guest(user) + another_project.add_maintainer(user) end include_context 'import applied' @@ -640,7 +640,11 @@ RSpec.describe Projects::ProjectMembersController do end end - context 'when user is not member of a source project' do + context "when user can't admin source project members" do + before do + another_project.add_developer(user) + end + include_context 'import applied' it 'does not import team members' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index a7aa0f1a1b8..2bb5fad9231 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -419,6 +419,47 @@ RSpec.describe ProjectsController do end end + describe 'POST create' do + let!(:params) do + { + path: 'foo', + description: 'bar', + import_url: project.http_url_to_repo, + namespace_id: user.namespace.id + } + end + + subject { post :create, params: { project: params } } + + before do + sign_in(user) + end + + context 'when import by url is disabled' do + before do + stub_application_setting(import_sources: []) + end + + it 'does not create project and reports an error' do + expect { subject }.not_to change { Project.count } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when import by url is enabled' do + before do + stub_application_setting(import_sources: ['git']) + end + + it 'creates project' do + expect { subject }.to change { Project.count } + + expect(response).to have_gitlab_http_status(:redirect) + end + end + end + describe 'GET edit' do it 'allows an admin user to access the page', :enable_admin_mode do sign_in(create(:user, :admin)) diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 043fd97f1ad..2aa9b86b20e 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -666,6 +666,6 @@ RSpec.describe UploadsController do def post_authorize(verified: true) request.headers.merge!(workhorse_internal_api_request_header) if verified - post :authorize, params: { model: 'personal_snippet', id: model.id }, format: :json + post :authorize, params: params, format: :json end end diff --git a/spec/features/callouts/security_newsletter_callout_spec.rb b/spec/features/callouts/security_newsletter_callout_spec.rb new file mode 100644 index 00000000000..b17bb372456 --- /dev/null +++ b/spec/features/callouts/security_newsletter_callout_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Security newsletter callout', :js do + let_it_be(:admin) { create(:admin) } + let_it_be(:non_admin) { create(:user) } + + shared_examples 'hidden callout' do + it 'does not display callout' do + expect(page).not_to have_content 'Sign up for the GitLab Security Newsletter to get notified for security updates.' + end + end + + context 'when an admin is logged in' do + before do + sign_in(admin) + gitlab_enable_admin_mode_sign_in(admin) + + visit admin_root_path + end + + it 'displays callout' do + expect(page).to have_content 'Sign up for the GitLab Security Newsletter to get notified for security updates.' + expect(page).to have_link 'Sign up for the GitLab newsletter', href: 'https://about.gitlab.com/company/preference-center/' + end + + context 'when link is clicked' do + before do + find_link('Sign up for the GitLab newsletter').click + + visit admin_root_path + end + + it_behaves_like 'hidden callout' + end + + context 'when callout is dismissed' do + before do + find('[data-testid="close-security-newsletter-callout"]').click + + visit admin_root_path + end + + it_behaves_like 'hidden callout' + end + end + + context 'when a non-admin is logged in' do + before do + sign_in(non_admin) + visit admin_root_path + end + + it_behaves_like 'hidden callout' + end +end diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index 275a87ca391..d2bde320c54 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js do click_button "Check out branch" - expect(page).to have_content 'git checkout -b "orphaned-branch" "origin/orphaned-branch"' + expect(page).to have_content 'git checkout -b \'orphaned-branch\' \'origin/orphaned-branch\'' end it 'allows filtering multiple dropdowns' do diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb index c9059395377..893dd2c76e0 100644 --- a/spec/features/profiles/password_spec.rb +++ b/spec/features/profiles/password_spec.rb @@ -78,40 +78,80 @@ RSpec.describe 'Profile > Password' do end end - context 'Change passowrd' do + context 'Change password' do + let(:new_password) { '22233344' } + before do sign_in(user) visit(edit_profile_password_path) end - it 'does not change user passowrd without old one' do - page.within '.update-password' do - fill_passwords('22233344', '22233344') + shared_examples 'user enters an incorrect current password' do + subject do + page.within '.update-password' do + fill_in 'user_current_password', with: user_current_password + fill_passwords(new_password, new_password) + end end - page.within '.flash-container' do - expect(page).to have_content 'You must provide a valid current password' - end - end + it 'handles the invalid password attempt, and prompts the user to try again', :aggregate_failures do + expect(Gitlab::AppLogger).to receive(:info) + .with(message: 'Invalid current password when attempting to update user password', username: user.username, ip: user.current_sign_in_ip) + + subject + + user.reload - it 'does not change password with invalid old password' do - page.within '.update-password' do - fill_in 'user_current_password', with: 'invalid' - fill_passwords('password', 'confirmation') + expect(user.failed_attempts).to eq(1) + expect(user.valid_password?(new_password)).to eq(false) + expect(current_path).to eq(edit_profile_password_path) + + page.within '.flash-container' do + expect(page).to have_content('You must provide a valid current password') + end end - page.within '.flash-container' do - expect(page).to have_content 'You must provide a valid current password' + it 'locks the user account when user passes the maximum attempts threshold', :aggregate_failures do + user.update!(failed_attempts: User.maximum_attempts.pred) + + subject + + expect(current_path).to eq(new_user_session_path) + + page.within '.flash-container' do + expect(page).to have_content('Your account is locked.') + end end end - it 'changes user password' do - page.within '.update-password' do - fill_in "user_current_password", with: user.password - fill_passwords('22233344', '22233344') + context 'when current password is blank' do + let(:user_current_password) { nil } + + it_behaves_like 'user enters an incorrect current password' + end + + context 'when current password is incorrect' do + let(:user_current_password) {'invalid' } + + it_behaves_like 'user enters an incorrect current password' + end + + context 'when the password reset is successful' do + subject do + page.within '.update-password' do + fill_in "user_current_password", with: user.password + fill_passwords(new_password, new_password) + end end - expect(current_path).to eq new_user_session_path + it 'changes the password, logs the user out and prompts them to sign in again', :aggregate_failures do + expect { subject }.to change { user.reload.valid_password?(new_password) }.to(true) + expect(current_path).to eq new_user_session_path + + page.within '.flash-container' do + expect(page).to have_content('Password was successfully updated. Please sign in again.') + end + end end end diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb new file mode 100644 index 00000000000..e1feca5031a --- /dev/null +++ b/spec/features/profiles/two_factor_auths_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Two factor auths' do + context 'when signed in' do + before do + allow(Gitlab).to receive(:com?) { true } + end + + context 'when user has two-factor authentication disabled' do + let(:user) { create(:user ) } + + before do + sign_in(user) + end + + it 'requires the current password to set up two factor authentication', :js do + visit profile_two_factor_auth_path + + register_2fa(user.reload.current_otp, '123') + + expect(page).to have_content('You must provide a valid current password') + + register_2fa(user.reload.current_otp, user.password) + + expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.') + + click_button 'Copy codes' + click_link 'Proceed' + + expect(page).to have_content('Status: Enabled') + end + end + + context 'when user has two-factor authentication enabled' do + let(:user) { create(:user, :two_factor) } + + before do + sign_in(user) + end + + it 'requires the current_password to disable two-factor authentication', :js do + visit profile_two_factor_auth_path + + fill_in 'current_password', with: '123' + + click_button 'Disable two-factor authentication' + + page.accept_alert + + expect(page).to have_content('You must provide a valid current password') + + fill_in 'current_password', with: user.password + + click_button 'Disable two-factor authentication' + + page.accept_alert + + expect(page).to have_content('Two-factor authentication has been disabled successfully!') + expect(page).to have_content('Enable two-factor authentication') + end + + it 'requires the current_password to regernate recovery codes', :js do + visit profile_two_factor_auth_path + + fill_in 'current_password', with: '123' + + click_button 'Regenerate recovery codes' + + expect(page).to have_content('You must provide a valid current password') + + fill_in 'current_password', with: user.password + + click_button 'Regenerate recovery codes' + + expect(page).to have_content('Please copy, download, or print your recovery codes before proceeding.') + end + end + + def register_2fa(pin, password) + fill_in 'pin_code', with: pin + fill_in 'current_password', with: password + + click_button 'Register with two-factor app' + end + end +end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 2c88860aef2..d9c919dae3d 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -807,6 +807,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do expect(current_path).to eq(profile_two_factor_auth_path) fill_in 'pin_code', with: user.reload.current_otp + fill_in 'current_password', with: user.password click_button 'Register with two-factor app' click_button 'Copy codes' diff --git a/spec/fixtures/api/schemas/public_api/v4/service.json b/spec/fixtures/api/schemas/public_api/v4/integration.json index b6f13d1cfe7..b6f13d1cfe7 100644 --- a/spec/fixtures/api/schemas/public_api/v4/service.json +++ b/spec/fixtures/api/schemas/public_api/v4/integration.json diff --git a/spec/fixtures/api/schemas/public_api/v4/integrations.json b/spec/fixtures/api/schemas/public_api/v4/integrations.json new file mode 100644 index 00000000000..e7ebe7652c9 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/integrations.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "integration.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/services.json b/spec/fixtures/api/schemas/public_api/v4/services.json deleted file mode 100644 index 78c59ecfa10..00000000000 --- a/spec/fixtures/api/schemas/public_api/v4/services.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "array", - "items": { "$ref": "service.json" } -} diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json index e3aeace6383..1072e63b20b 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/project.json +++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json @@ -7579,23 +7579,6 @@ } } ], - "triggers": [ - { - "id": 123, - "token": "cdbfasdf44a5958c83654733449e585", - "project_id": 5, - "owner_id": 1, - "created_at": "2017-01-16T15:25:28.637Z", - "updated_at": "2017-01-16T15:25:28.637Z" - }, - { - "id": 456, - "token": "33a66349b5ad01fc00174af87804e40", - "project_id": 5, - "created_at": "2017-01-16T15:25:29.637Z", - "updated_at": "2017-01-16T15:25:29.637Z" - } - ], "pipeline_schedules": [ { "id": 1, diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson index 93619f4fb44..2b5bda687b8 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson +++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson @@ -1,2 +1,2 @@ {"id":123,"token":"cdbfasdf44a5958c83654733449e585","project_id":5,"owner_id":1,"created_at":"2017-01-16T15:25:28.637Z","updated_at":"2017-01-16T15:25:28.637Z"} -{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"} +{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"}
\ No newline at end of file diff --git a/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap b/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap new file mode 100644 index 00000000000..3fe0e570a54 --- /dev/null +++ b/spec/frontend/authentication/two_factor_auth/components/__snapshots__/manage_two_factor_form_spec.js.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ManageTwoFactorForm Disable button renders the component correctly 1`] = ` +VueWrapper { + "_emitted": Object {}, + "_emittedByOrder": Array [], + "isFunctionalComponent": undefined, +} +`; + +exports[`ManageTwoFactorForm Disable button renders the component correctly 2`] = ` +<form + action="#" + class="gl-display-inline-block" + method="post" +> + <input + data-testid="test-2fa-method-field" + name="_method" + type="hidden" + /> + + <input + name="authenticity_token" + type="hidden" + /> + + <div + class="form-group gl-form-group" + id="__BVID__15" + role="group" + > + <label + class="d-block col-form-label" + for="current-password" + id="__BVID__15__BV_label_" + > + Current password + </label> + <div + class="bv-no-focus-ring" + > + <input + aria-required="true" + class="gl-form-input form-control" + data-qa-selector="current_password_field" + id="current-password" + name="current_password" + required="required" + type="password" + /> + <!----> + <!----> + <!----> + </div> + </div> + + <button + class="btn btn-danger gl-mr-3 gl-display-inline-block btn-danger btn-md gl-button" + data-confirm="Are you sure? This will invalidate your registered applications and U2F devices." + data-form-action="2fa_auth_path" + data-form-method="2fa_auth_method" + data-testid="test-2fa-disable-button" + type="submit" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Disable two-factor authentication + + </span> + </button> + + <button + class="btn gl-display-inline-block btn-default btn-md gl-button" + data-form-action="2fa_codes_path" + data-form-method="2fa_codes_method" + data-testid="test-2fa-regenerate-codes-button" + type="submit" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + Regenerate recovery codes + + </span> + </button> +</form> +`; diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js new file mode 100644 index 00000000000..384579c6876 --- /dev/null +++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js @@ -0,0 +1,98 @@ +import { within } from '@testing-library/dom'; +import { mount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import ManageTwoFactorForm, { + i18n, +} from '~/authentication/two_factor_auth/components/manage_two_factor_form.vue'; + +describe('ManageTwoFactorForm', () => { + let wrapper; + + const createComponent = (options = {}) => { + wrapper = extendedWrapper( + mount(ManageTwoFactorForm, { + provide: { + webauthnEnabled: options?.webauthnEnabled || false, + profileTwoFactorAuthPath: '2fa_auth_path', + profileTwoFactorAuthMethod: '2fa_auth_method', + codesProfileTwoFactorAuthPath: '2fa_codes_path', + codesProfileTwoFactorAuthMethod: '2fa_codes_method', + }, + }), + ); + }; + + const queryByText = (text, options) => within(wrapper.element).queryByText(text, options); + const queryByLabelText = (text, options) => + within(wrapper.element).queryByLabelText(text, options); + + beforeEach(() => { + createComponent(); + }); + + describe('Current password field', () => { + it('renders the current password field', () => { + expect(queryByLabelText(i18n.currentPassword).tagName).toEqual('INPUT'); + }); + }); + + describe('Disable button', () => { + it('renders the component correctly', () => { + expect(wrapper).toMatchSnapshot(); + expect(wrapper.element).toMatchSnapshot(); + }); + + it('has the right confirm text', () => { + expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual( + i18n.confirm, + ); + }); + + describe('when webauthnEnabled', () => { + beforeEach(() => { + createComponent({ + webauthnEnabled: true, + }); + }); + + it('has the right confirm text', () => { + expect(wrapper.findByTestId('test-2fa-disable-button').element.dataset.confirm).toEqual( + i18n.confirmWebAuthn, + ); + }); + }); + + it('modifies the form action and method when submitted through the button', async () => { + const form = wrapper.find('form'); + const disableButton = wrapper.findByTestId('test-2fa-disable-button').element; + const methodInput = wrapper.findByTestId('test-2fa-method-field').element; + + form.trigger('submit', { submitter: disableButton }); + + await wrapper.vm.$nextTick(); + + expect(form.element.getAttribute('action')).toEqual('2fa_auth_path'); + expect(methodInput.getAttribute('value')).toEqual('2fa_auth_method'); + }); + }); + + describe('Regenerate recovery codes button', () => { + it('renders the button', () => { + expect(queryByText(i18n.regenerateRecoveryCodes)).toEqual(expect.any(HTMLElement)); + }); + + it('modifies the form action and method when submitted through the button', async () => { + const form = wrapper.find('form'); + const regenerateCodesButton = wrapper.findByTestId('test-2fa-regenerate-codes-button') + .element; + const methodInput = wrapper.findByTestId('test-2fa-method-field').element; + + form.trigger('submit', { submitter: regenerateCodesButton }); + + await wrapper.vm.$nextTick(); + + expect(form.element.getAttribute('action')).toEqual('2fa_codes_path'); + expect(methodInput.getAttribute('value')).toEqual('2fa_codes_method'); + }); + }); +}); diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 211ed064762..94ad7759110 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -574,6 +574,15 @@ describe('GfmAutoComplete', () => { }), ).toBe('<li><small>grp/proj#5</small> Some Issue</li>'); }); + + it('escapes title in the template as it is user input', () => { + expect( + GfmAutoComplete.Issues.templateFunction({ + id: 5, + title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string + }), + ).toBe('<li><small>5</small> ${search}<script>oh no $</li>'); + }); }); describe('GfmAutoComplete.Members', () => { @@ -608,16 +617,18 @@ describe('GfmAutoComplete', () => { ).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>'); }); - it('should add escaped title if title is set', () => { + it('escapes title in the template as it is user input', () => { expect( GfmAutoComplete.Members.templateFunction({ avatarTag: 'IMG', username: 'my-group', - title: 'MyGroup+', + title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string icon: '<i class="icon"/>', availabilityStatus: '', }), - ).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>'); + ).toBe( + '<li>IMG my-group <small>${search}<script>oh no $</small> <i class="icon"/></li>', + ); }); it('should add user availability status if availabilityStatus is set', () => { @@ -782,6 +793,15 @@ describe('GfmAutoComplete', () => { ${'/unlabel ~'} | ${assignedLabels} `('$input shows $output.length labels', expectLabels); }); + + it('escapes title in the template as it is user input', () => { + const color = '#123456'; + const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string + + expect(GfmAutoComplete.Labels.templateFunction(color, title)).toBe( + '<li><span class="dropdown-label-box" style="background: #123456"></span> ${search}<script>oh no $</li>', + ); + }); }); describe('emoji', () => { @@ -829,4 +849,15 @@ describe('GfmAutoComplete', () => { }); }); }); + + describe('milestones', () => { + it('escapes title in the template as it is user input', () => { + const expired = false; + const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string + + expect(GfmAutoComplete.Milestones.templateFunction(title, expired)).toBe( + '<li>${search}<script>oh no $</li>', + ); + }); + }); }); diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 1f3659b5c76..9570d2a831c 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -363,4 +363,25 @@ describe('text_utility', () => { expect(textUtils.insertFinalNewline(input, '\r\n')).toBe(output); }); }); + + describe('escapeShellString', () => { + it.each` + character | input | output + ${'"'} | ${'";echo "you_shouldnt_run_this'} | ${'\'";echo "you_shouldnt_run_this\''} + ${'$'} | ${'$IFS'} | ${"'$IFS'"} + ${'\\'} | ${'evil-branch-name\\'} | ${"'evil-branch-name\\'"} + ${'!'} | ${'!event'} | ${"'!event'"} + `( + 'should not escape the $character character but wrap in single-quotes', + ({ input, output }) => { + expect(textUtils.escapeShellString(input)).toBe(output); + }, + ); + + it("should escape the ' character and wrap in single-quotes", () => { + expect(textUtils.escapeShellString("fix-'bug-behavior'")).toBe( + "'fix-'\\''bug-behavior'\\'''", + ); + }); + }); }); diff --git a/spec/frontend/users_select/index_spec.js b/spec/frontend/users_select/index_spec.js index 99caaf61c54..0d2aae78944 100644 --- a/spec/frontend/users_select/index_spec.js +++ b/spec/frontend/users_select/index_spec.js @@ -1,3 +1,5 @@ +import { escape } from 'lodash'; +import UsersSelect from '~/users_select/index'; import { createInputsModelExpectation, createUnassignedExpectation, @@ -91,5 +93,19 @@ describe('~/users_select/index', () => { expect(findDropdownItemsModel()).toEqual(expectation); }); }); + + describe('renderApprovalRules', () => { + const ruleNames = ['simple-name', '"\'<>&', '"><script>alert(1)<script>']; + + it.each(ruleNames)('escapes rule name correctly for %s', (name) => { + const escapedName = escape(name); + + expect( + UsersSelect.prototype.renderApprovalRules('reviewer', [{ name }]), + ).toMatchInterpolatedText( + `<div class="gl-display-flex gl-font-sm"> <span class="gl-text-truncate" title="${escapedName}">${escapedName}</span> </div>`, + ); + }); + }); }); }); diff --git a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js index bd22183cbea..913d5860b48 100644 --- a/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js +++ b/spec/frontend/vue_mr_widget/mr_widget_how_to_merge_modal_spec.js @@ -8,11 +8,9 @@ describe('MRWidgetHowToMerge', () => { function mountComponent({ data = {}, props = {} } = {}) { wrapper = shallowMount(MrWidgetHowToMergeModal, { data() { - return { ...data }; - }, - propsData: { - ...props, + return data; }, + propsData: props, stubs: {}, }); } @@ -57,4 +55,16 @@ describe('MRWidgetHowToMerge', () => { mountComponent({ props: { isFork: true } }); expect(findInstructionsFields().at(0).text()).toContain('FETCH_HEAD'); }); + + it('escapes the target branch name shell-secure', () => { + mountComponent({ props: { targetBranch: '";echo$IFS"you_shouldnt_run_this' } }); + + expect(findInstructionsFields().at(1).text()).toContain('\'";echo$IFS"you_shouldnt_run_this\''); + }); + + it('escapes the source branch name shell-secure', () => { + mountComponent({ props: { sourceBranch: 'branch-of-$USER' } }); + + expect(findInstructionsFields().at(0).text()).toContain("'branch-of-$USER'"); + }); }); diff --git a/spec/frontend_integration/fixture_generators.yml b/spec/frontend_integration/fixture_generators.yml new file mode 100644 index 00000000000..1f6ff85352d --- /dev/null +++ b/spec/frontend_integration/fixture_generators.yml @@ -0,0 +1,5 @@ +- spec/frontend/fixtures/api_projects.rb +- spec/frontend/fixtures/api_merge_requests.rb +- spec/frontend/fixtures/projects_json.rb +- spec/frontend/fixtures/merge_requests_diffs.rb +- spec/frontend/fixtures/raw.rb diff --git a/spec/graphql/types/group_invitation_type_spec.rb b/spec/graphql/types/group_invitation_type_spec.rb index dab2d43fc90..9eedc2db81d 100644 --- a/spec/graphql/types/group_invitation_type_spec.rb +++ b/spec/graphql/types/group_invitation_type_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Types::GroupInvitationType do specify { expect(described_class.graphql_name).to eq('GroupInvitation') } - specify { expect(described_class).to require_graphql_authorizations(:read_group) } + specify { expect(described_class).to require_graphql_authorizations(:admin_group) } it 'has the expected fields' do expected_fields = %w[ diff --git a/spec/graphql/types/project_invitation_type_spec.rb b/spec/graphql/types/project_invitation_type_spec.rb index 148a763a5fa..5c0b03c2505 100644 --- a/spec/graphql/types/project_invitation_type_spec.rb +++ b/spec/graphql/types/project_invitation_type_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Types::ProjectInvitationType do specify { expect(described_class.graphql_name).to eq('ProjectInvitation') } - specify { expect(described_class).to require_graphql_authorizations(:read_project) } + specify { expect(described_class).to require_graphql_authorizations(:admin_project) } it 'has the expected fields' do expected_fields = %w[ diff --git a/spec/helpers/external_link_helper_spec.rb b/spec/helpers/external_link_helper_spec.rb index f5bb0568824..b746cb04ab3 100644 --- a/spec/helpers/external_link_helper_spec.rb +++ b/spec/helpers/external_link_helper_spec.rb @@ -13,8 +13,14 @@ RSpec.describe ExternalLinkHelper do it 'allows options when creating external link with icon' do link = external_link('https://gitlab.com', 'https://gitlab.com', { "data-foo": "bar", class: "externalLink" }).to_s - expect(link).to start_with('<a target="_blank" rel="noopener noreferrer" data-foo="bar" class="externalLink" href="https://gitlab.com">https://gitlab.com') expect(link).to include('data-testid="external-link-icon"') end + + it 'sanitizes and returns external link with icon' do + link = external_link('sanitized link content', 'javascript:alert()').to_s + expect(link).not_to include('href="javascript:alert()"') + expect(link).to start_with('<a target="_blank" rel="noopener noreferrer">sanitized link content') + expect(link).to include('data-testid="external-link-icon"') + end end diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 4784d0aff26..af2957d72c7 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -35,22 +35,22 @@ RSpec.describe IconsHelper do it 'returns svg icon html with DEFAULT_ICON_SIZE' do expect(sprite_icon(icon_name).to_s) - .to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" + .to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>" end it 'returns svg icon html without size class' do expect(sprite_icon(icon_name, size: nil).to_s) - .to eq "<svg data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" + .to eq "<svg data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>" end it 'returns svg icon html + size classes' do expect(sprite_icon(icon_name, size: 72).to_s) - .to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" + .to eq "<svg class=\"s72\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>" end it 'returns svg icon html + size classes + additional class' do expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s) - .to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" + .to eq "<svg class=\"s72 icon-danger\" data-testid=\"#{icon_name}-icon\"><use href=\"#{icons_path}##{icon_name}\"></use></svg>" end describe 'non existing icon' do diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb index 794ff5ee945..f738ba855b8 100644 --- a/spec/helpers/user_callouts_helper_spec.rb +++ b/spec/helpers/user_callouts_helper_spec.rb @@ -293,4 +293,37 @@ RSpec.describe UserCalloutsHelper do it { is_expected.to eq(false) } end end + + describe '.show_security_newsletter_user_callout?' do + let_it_be(:admin) { create(:user, :admin) } + + subject { helper.show_security_newsletter_user_callout? } + + context 'when `current_user` is not an admin' do + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:user_dismissed?).with(described_class::SECURITY_NEWSLETTER_CALLOUT) { false } + end + + it { is_expected.to be false } + end + + context 'when user has dismissed callout' do + before do + allow(helper).to receive(:current_user).and_return(admin) + allow(helper).to receive(:user_dismissed?).with(described_class::SECURITY_NEWSLETTER_CALLOUT) { true } + end + + it { is_expected.to be false } + end + + context 'when `current_user` is an admin and user has not dismissed callout' do + before do + allow(helper).to receive(:current_user).and_return(admin) + allow(helper).to receive(:user_dismissed?).with(described_class::SECURITY_NEWSLETTER_CALLOUT) { false } + end + + it { is_expected.to be true } + end + end end diff --git a/spec/initializers/100_patch_omniauth_oauth2_spec.rb b/spec/initializers/100_patch_omniauth_oauth2_spec.rb new file mode 100644 index 00000000000..0c436e4ef45 --- /dev/null +++ b/spec/initializers/100_patch_omniauth_oauth2_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'OmniAuth::Strategies::OAuth2', type: :strategy do + let(:strategy) { [OmniAuth::Strategies::OAuth2] } + + it 'verifies the gem version' do + current_version = OmniAuth::OAuth2::VERSION + expected_version = '1.7.1' + + expect(current_version).to eq(expected_version), <<~EOF + New version #{current_version} of the `omniauth-oauth2` gem detected! + + Please check if the monkey patches in `config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb` + are still needed, and either update/remove them, or bump the version in this spec. + + EOF + end + + context 'when a custom error message is passed from an OAuth2 provider' do + let(:message) { 'Please go to https://evil.com' } + let(:state) { 'secret' } + let(:callback_path) { '/users/auth/oauth2/callback' } + let(:params) { { state: state, error: 'evil_key', error_description: message } } + let(:error) { last_request.env['omniauth.error'] } + + before do + env('rack.session', { 'omniauth.state' => state }) + end + + it 'returns the custom error message if the state is valid' do + get callback_path, **params + + expect(error.message).to eq("evil_key | #{message}") + end + + it 'returns the custom `error_reason` message if the `error_description` is blank' do + get callback_path, **params.merge(error_description: ' ', error_reason: 'custom reason') + + expect(error.message).to eq('evil_key | custom reason') + end + + it 'returns a CSRF error if the state is invalid' do + get callback_path, **params.merge(state: 'invalid') + + expect(error.message).to eq('csrf_detected | CSRF detected') + end + + it 'returns a CSRF error if the state is missing' do + get callback_path, **params.without(:state) + + expect(error.message).to eq('csrf_detected | CSRF detected') + end + end +end diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb index 2c64657d69d..820ebeb6945 100644 --- a/spec/lib/banzai/filter/spaced_link_filter_spec.rb +++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb @@ -63,6 +63,16 @@ RSpec.describe Banzai::Filter::SpacedLinkFilter do end end + it 'does not process malicious input' do + Timeout.timeout(10) do + doc = filter('[ (](' * 60_000) + + found_links = doc.css('a') + + expect(found_links.size).to eq(0) + end + end + it 'converts multiple URLs' do link1 = '[first](slug one)' link2 = '[second](http://example.com/slug two)' diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb index 57a258b0d9f..7d156c2c3df 100644 --- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb +++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb @@ -186,4 +186,20 @@ RSpec.describe BulkImports::NdjsonPipeline do end end end + + describe '#relation_factory' do + context 'when portable is group' do + it 'returns group relation factory' do + expect(subject.relation_factory).to eq(Gitlab::ImportExport::Group::RelationFactory) + end + end + + context 'when portable is project' do + subject { NdjsonPipelineClass.new(project, user) } + + it 'returns project relation factory' do + expect(subject.relation_factory).to eq(Gitlab::ImportExport::Project::RelationFactory) + end + end + end end diff --git a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb new file mode 100644 index 00000000000..97fcddefd42 --- /dev/null +++ b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:entity) do + create( + :bulk_import_entity, + :project_entity, + project: project, + bulk_import: bulk_import, + source_full_path: 'source/full/path', + destination_name: 'My Destination Project', + destination_namespace: group.full_path + ) + end + + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } + + let(:issue_attributes) { {} } + let(:issue) do + { + 'iid' => 7, + 'title' => 'Imported Issue', + 'description' => 'Description', + 'state' => 'opened', + 'updated_at' => '2016-06-14T15:02:47.967Z', + 'author_id' => 22 + }.merge(issue_attributes) + end + + subject(:pipeline) { described_class.new(context) } + + describe '#run' do + before do + group.add_owner(user) + issue_with_index = [issue, 0] + + allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [issue_with_index])) + end + + pipeline.run + end + + it 'imports issue into destination project' do + expect(project.issues.count).to eq(1) + + imported_issue = project.issues.last + + aggregate_failures do + expect(imported_issue.iid).to eq(7) + expect(imported_issue.title).to eq(issue['title']) + expect(imported_issue.description).to eq(issue['description']) + expect(imported_issue.author).to eq(user) + expect(imported_issue.state).to eq('opened') + expect(imported_issue.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') + end + end + + context 'zoom meetings' do + let(:issue_attributes) { { 'zoom_meetings' => [{ 'url' => 'https://zoom.us/j/123456789' }] } } + + it 'restores zoom meetings' do + expect(project.issues.last.zoom_meetings.first.url).to eq('https://zoom.us/j/123456789') + end + end + + context 'sentry issue' do + let(:issue_attributes) { { 'sentry_issue' => { 'sentry_issue_identifier' => '1234567891' } } } + + it 'restores sentry issue information' do + expect(project.issues.last.sentry_issue.sentry_issue_identifier).to eq(1234567891) + end + end + + context 'award emoji' do + let(:issue_attributes) { { 'award_emoji' => [{ 'name' => 'musical_keyboard', 'user_id' => 22 }] } } + + it 'has award emoji on an issue' do + award_emoji = project.issues.last.award_emoji.first + + expect(award_emoji.name).to eq('musical_keyboard') + expect(award_emoji.user).to eq(user) + end + end + context 'issue state' do + let(:issue_attributes) { { 'state' => 'closed' } } + + it 'restores issue state' do + expect(project.issues.last.state).to eq('closed') + end + end + + context 'labels' do + let(:issue_attributes) do + { + 'label_links' => [ + { 'label' => { 'title' => 'imported label 1', 'type' => 'ProjectLabel' } }, + { 'label' => { 'title' => 'imported label 2', 'type' => 'ProjectLabel' } } + ] + } + end + + it 'restores issue labels' do + expect(project.issues.last.labels.pluck(:title)).to contain_exactly('imported label 1', 'imported label 2') + end + end + + context 'milestone' do + let(:issue_attributes) { { 'milestone' => { 'title' => 'imported milestone' } } } + + it 'restores issue milestone' do + expect(project.issues.last.milestone.title).to eq('imported milestone') + end + end + + context 'timelogs' do + let(:issue_attributes) { { 'timelogs' => [{ 'time_spent' => 72000, 'spent_at' => '2019-12-27T00:00:00.000Z', 'user_id' => 22 }] } } + + it 'restores issue timelogs' do + timelog = project.issues.last.timelogs.first + + aggregate_failures do + expect(timelog.time_spent).to eq(72000) + expect(timelog.spent_at).to eq("2019-12-27T00:00:00.000Z") + end + end + end + + context 'notes' do + let(:issue_attributes) do + { + 'notes' => [ + { + 'note' => 'Issue note', + 'author_id' => 22, + 'author' => { + 'name' => 'User 22' + }, + 'updated_at' => '2016-06-14T15:02:47.770Z', + 'award_emoji' => [ + { + 'name' => 'clapper', + 'user_id' => 22 + } + ] + } + ] + } + end + + it 'restores issue notes and their award emoji' do + note = project.issues.last.notes.first + + aggregate_failures do + expect(note.note).to eq("Issue note\n\n *By User 22 on 2016-06-14T15:02:47 (imported from GitLab)*") + expect(note.award_emoji.first.name).to eq('clapper') + end + end + end + end +end diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb index d74f72cee03..0109b65427f 100644 --- a/spec/lib/bulk_imports/projects/stage_spec.rb +++ b/spec/lib/bulk_imports/projects/stage_spec.rb @@ -7,7 +7,8 @@ RSpec.describe BulkImports::Projects::Stage do [ [0, BulkImports::Projects::Pipelines::ProjectPipeline], [1, BulkImports::Common::Pipelines::LabelsPipeline], - [2, BulkImports::Common::Pipelines::EntityFinisher] + [2, BulkImports::Projects::Pipelines::IssuesPipeline], + [3, BulkImports::Common::Pipelines::EntityFinisher] ] end diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb index 28e93a8da52..2543eb3a5e9 100644 --- a/spec/lib/gitlab/auth/request_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -81,32 +81,72 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do expect(subject.find_sessionless_user(:api)).to eq job_token_user end - it 'returns lfs_token user if no job_token user found' do - allow_any_instance_of(described_class) - .to receive(:find_user_from_lfs_token) - .and_return(lfs_token_user) - - expect(subject.find_sessionless_user(:api)).to eq lfs_token_user - end - - it 'returns basic_auth_access_token user if no lfs_token user found' do + it 'returns nil even if basic_auth_access_token is available' do allow_any_instance_of(described_class) .to receive(:find_user_from_personal_access_token) .and_return(basic_auth_access_token_user) - expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user + expect(subject.find_sessionless_user(:api)).to be_nil end - it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do + it 'returns nil even if find_user_from_lfs_token is available' do allow_any_instance_of(described_class) - .to receive(:find_user_from_basic_auth_password) - .and_return(basic_auth_password_user) + .to receive(:find_user_from_lfs_token) + .and_return(lfs_token_user) - expect(subject.find_sessionless_user(:api)).to eq basic_auth_password_user + expect(subject.find_sessionless_user(:api)).to be_nil end it 'returns nil if no user found' do - expect(subject.find_sessionless_user(:api)).to be_blank + expect(subject.find_sessionless_user(:api)).to be_nil + end + + context 'in an API request' do + before do + env['SCRIPT_NAME'] = '/api/v4/projects' + end + + it 'returns basic_auth_access_token user if no job_token_user found' do + allow_any_instance_of(described_class) + .to receive(:find_user_from_personal_access_token) + .and_return(basic_auth_access_token_user) + + expect(subject.find_sessionless_user(:api)).to eq basic_auth_access_token_user + end + end + + context 'in a Git request' do + before do + env['SCRIPT_NAME'] = '/group/project.git/info/refs' + end + + it 'returns lfs_token user if no job_token user found' do + allow_any_instance_of(described_class) + .to receive(:find_user_from_lfs_token) + .and_return(lfs_token_user) + + expect(subject.find_sessionless_user(nil)).to eq lfs_token_user + end + + it 'returns basic_auth_access_token user if no lfs_token user found' do + allow_any_instance_of(described_class) + .to receive(:find_user_from_personal_access_token) + .and_return(basic_auth_access_token_user) + + expect(subject.find_sessionless_user(nil)).to eq basic_auth_access_token_user + end + + it 'returns basic_auth_access_password user if no basic_auth_access_token user found' do + allow_any_instance_of(described_class) + .to receive(:find_user_from_basic_auth_password) + .and_return(basic_auth_password_user) + + expect(subject.find_sessionless_user(nil)).to eq basic_auth_password_user + end + + it 'returns nil if no user found' do + expect(subject.find_sessionless_user(nil)).to be_blank + end end it 'rescue Gitlab::Auth::AuthenticationError exceptions' do diff --git a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb index f906870195a..876c23a91bd 100644 --- a/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb +++ b/spec/lib/gitlab/auth/two_factor_auth_verifier_spec.rb @@ -3,33 +3,50 @@ require 'spec_helper' RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do - let(:user) { create(:user) } + using RSpec::Parameterized::TableSyntax - subject { described_class.new(user) } + subject(:verifier) { described_class.new(user) } - describe '#two_factor_authentication_required?' do - describe 'when it is required on application level' do - it 'returns true' do - stub_application_setting require_two_factor_authentication: true + let(:user) { build_stubbed(:user, otp_grace_period_started_at: Time.zone.now) } - expect(subject.two_factor_authentication_required?).to be_truthy - end - end + describe '#two_factor_authentication_enforced?' do + subject { verifier.two_factor_authentication_enforced? } - describe 'when it is required on group level' do - it 'returns true' do - allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(true) + where(:instance_level_enabled, :group_level_enabled, :grace_period_expired, :should_be_enforced) do + false | false | true | false + true | false | false | false + true | false | true | true + false | true | false | false + false | true | true | true + end - expect(subject.two_factor_authentication_required?).to be_truthy + with_them do + before do + stub_application_setting(require_two_factor_authentication: instance_level_enabled) + allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled) + stub_application_setting(two_factor_grace_period: grace_period_expired ? 0 : 1.month.in_hours) end + + it { is_expected.to eq(should_be_enforced) } end + end - describe 'when it is not required' do - it 'returns false when not required on group level' do - allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(false) + describe '#two_factor_authentication_required?' do + subject { verifier.two_factor_authentication_required? } + + where(:instance_level_enabled, :group_level_enabled, :should_be_required) do + true | false | true + false | true | true + false | false | false + end - expect(subject.two_factor_authentication_required?).to be_falsey + with_them do + before do + stub_application_setting(require_two_factor_authentication: instance_level_enabled) + allow(user).to receive(:require_two_factor_authentication_from_group?).and_return(group_level_enabled) end + + it { is_expected.to eq(should_be_required) } end end @@ -85,25 +102,21 @@ RSpec.describe Gitlab::Auth::TwoFactorAuthVerifier do end describe '#two_factor_grace_period_expired?' do - before do - allow(user).to receive(:otp_grace_period_started_at).and_return(4.hours.ago) - end - it 'returns true if the grace period has expired' do - allow(subject).to receive(:two_factor_grace_period).and_return(2) + stub_application_setting two_factor_grace_period: 0 expect(subject.two_factor_grace_period_expired?).to be_truthy end it 'returns false if the grace period has not expired' do - allow(subject).to receive(:two_factor_grace_period).and_return(6) + stub_application_setting two_factor_grace_period: 1.month.in_hours expect(subject.two_factor_grace_period_expired?).to be_falsey end context 'when otp_grace_period_started_at is nil' do it 'returns false' do - allow(user).to receive(:otp_grace_period_started_at).and_return(nil) + user.otp_grace_period_started_at = nil expect(subject.two_factor_grace_period_expired?).to be_falsey end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index cc592bb8f24..5ec6e23774a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -386,7 +386,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do shared_examples 'with an invalid access token' do it 'fails for a non-member' do expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip')) - .to have_attributes(auth_failure ) + .to have_attributes(auth_failure) end context 'when project bot user is blocked' do @@ -396,7 +396,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'fails for a blocked project bot' do expect(gl_auth.find_for_git_client(project_bot_user.username, access_token.token, project: project, ip: 'ip')) - .to have_attributes(auth_failure ) + .to have_attributes(auth_failure) end end end @@ -466,6 +466,41 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do .to have_attributes(auth_failure) end + context 'when 2fa is enabled globally' do + let_it_be(:user) do + create(:user, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago) + end + + before do + stub_application_setting(require_two_factor_authentication: true) + end + + it 'fails if grace period expired' do + stub_application_setting(two_factor_grace_period: 0) + + expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') } + .to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError) + end + + it 'goes through if grace period is not expired yet' do + stub_application_setting(two_factor_grace_period: 72) + + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) + .to have_attributes(actor: user, project: nil, type: :gitlab_or_ldap, authentication_abilities: described_class.full_authentication_abilities) + end + end + + context 'when 2fa is enabled personally' do + let(:user) do + create(:user, :two_factor, username: 'normal_user', password: 'my-secret', otp_grace_period_started_at: 1.day.ago) + end + + it 'fails' do + expect { gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip') } + .to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError) + end + end + it 'goes through lfs authentication' do user = create( :user, @@ -757,16 +792,16 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do describe 'find_with_user_password' do let!(:user) do create(:user, - username: username, - password: password, - password_confirmation: password) + username: username, + password: password, + password_confirmation: password) end let(:username) { 'John' } # username isn't lowercase, test this let(:password) { 'my-secret' } it "finds user by valid login/password" do - expect( gl_auth.find_with_user_password(username, password) ).to eql user + expect(gl_auth.find_with_user_password(username, password)).to eql user end it 'finds user by valid email/password with case-insensitive email' do @@ -779,12 +814,12 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it "does not find user with invalid password" do password = 'wrong' - expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + expect(gl_auth.find_with_user_password(username, password)).not_to eql user end it "does not find user with invalid login" do user = 'wrong' - expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + expect(gl_auth.find_with_user_password(username, password)).not_to eql user end include_examples 'user login operation with unique ip limit' do @@ -796,13 +831,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it 'finds the user in deactivated state' do user.deactivate! - expect( gl_auth.find_with_user_password(username, password) ).to eql user + expect(gl_auth.find_with_user_password(username, password)).to eql user end it "does not find user in blocked state" do user.block - expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + expect(gl_auth.find_with_user_password(username, password)).not_to eql user end it 'does not find user in locked state' do @@ -814,13 +849,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do it "does not find user in ldap_blocked state" do user.ldap_block - expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + expect(gl_auth.find_with_user_password(username, password)).not_to eql user end it 'does not find user in blocked_pending_approval state' do user.block_pending_approval - expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + expect(gl_auth.find_with_user_password(username, password)).not_to eql user end context 'with increment_failed_attempts' do diff --git a/spec/lib/gitlab/fogbugz_import/importer_spec.rb b/spec/lib/gitlab/fogbugz_import/importer_spec.rb index eb0c4da6ce3..9b58b772d1a 100644 --- a/spec/lib/gitlab/fogbugz_import/importer_spec.rb +++ b/spec/lib/gitlab/fogbugz_import/importer_spec.rb @@ -4,23 +4,11 @@ require 'spec_helper' RSpec.describe Gitlab::FogbugzImport::Importer do let(:project) { create(:project_empty_repo) } - let(:importer) { described_class.new(project) } - let(:repo) do - instance_double(Gitlab::FogbugzImport::Repository, - safe_name: 'vim', - path: 'vim', - raw_data: '') - end - - let(:import_data) { { 'repo' => repo } } - let(:credentials) do - { - 'fb_session' => { - 'uri' => 'https://testing.fogbugz.com', - 'token' => 'token' - } - } - end + let(:fogbugz_project) { { 'ixProject' => project.id, 'sProject' => 'vim' } } + let(:import_data) { { 'repo' => fogbugz_project } } + let(:base_url) { 'https://testing.fogbugz.com' } + let(:token) { 'token' } + let(:credentials) { { 'fb_session' => { 'uri' => base_url, 'token' => token } } } let(:closed_bug) do { @@ -46,18 +34,22 @@ RSpec.describe Gitlab::FogbugzImport::Importer do let(:fogbugz_bugs) { [opened_bug, closed_bug] } + subject(:importer) { described_class.new(project) } + before do project.create_import_data(data: import_data, credentials: credentials) - allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listCategories).and_return([]) - allow_any_instance_of(Gitlab::FogbugzImport::Client).to receive(:cases).and_return(fogbugz_bugs) + + stub_fogbugz('listProjects', projects: { project: [fogbugz_project], count: 1 }) + stub_fogbugz('listCategories', categories: { category: [], count: 0 }) + stub_fogbugz('search', cases: { case: fogbugz_bugs, count: fogbugz_bugs.size }) end it 'imports bugs' do - expect { importer.execute }.to change { Issue.count }.by(2) + expect { subject.execute }.to change { Issue.count }.by(2) end it 'imports opened bugs' do - importer.execute + subject.execute issue = Issue.where(project_id: project.id).find_by_title(opened_bug[:sTitle]) @@ -65,10 +57,54 @@ RSpec.describe Gitlab::FogbugzImport::Importer do end it 'imports closed bugs' do - importer.execute + subject.execute issue = Issue.where(project_id: project.id).find_by_title(closed_bug[:sTitle]) expect(issue.state_id).to eq(Issue.available_states[:closed]) end + + context 'verify url' do + context 'when host is localhost' do + let(:base_url) { 'https://localhost:3000' } + + it 'does not allow localhost requests' do + expect { subject.execute } + .to raise_error( + ::Gitlab::HTTP::BlockedUrlError, + "URL 'https://localhost:3000/api.asp' is blocked: Requests to localhost are not allowed" + ) + end + end + + context 'when host is on local network' do + let(:base_url) { 'http://192.168.0.1' } + + it 'does not allow localhost requests' do + expect { subject.execute } + .to raise_error( + ::Gitlab::HTTP::BlockedUrlError, + "URL 'http://192.168.0.1/api.asp' is blocked: Requests to the local network are not allowed" + ) + end + end + + context 'when host is ftp protocol' do + let(:base_url) { 'ftp://testing' } + + it 'only accept http and https requests' do + expect { subject.execute } + .to raise_error( + HTTParty::UnsupportedURIScheme, + "'ftp://testing/api.asp' Must be HTTP, HTTPS or Generic" + ) + end + end + end + + def stub_fogbugz(command, response) + stub_request(:post, "#{base_url}/api.asp") + .with(body: hash_including({ 'cmd' => command, 'token' => token })) + .to_return(status: 200, body: response.to_xml(root: :response)) + end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index bf682e4e4c6..bf2e3c7f5f8 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -435,17 +435,19 @@ RSpec.describe Gitlab::GitAccess do it 'disallows users with expired password to pull' do project.add_maintainer(user) - user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.minutes.ago) expect { pull_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.") end - it 'allows ldap users with expired password to pull' do - project.add_maintainer(user) - user.update!(password_expires_at: 2.minutes.ago) - allow(user).to receive(:ldap_user?).and_return(true) + context 'with an ldap user' do + let(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) } - expect { pull_access_check }.not_to raise_error + it 'allows ldap users with expired password to pull' do + project.add_maintainer(user) + + expect { pull_access_check }.not_to raise_error + end end context 'when the project repository does not exist' do @@ -987,24 +989,23 @@ RSpec.describe Gitlab::GitAccess do end it 'disallows users with expired password to push' do - user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.minutes.ago) expect { push_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.") end - it 'allows ldap users with expired password to push' do - user.update!(password_expires_at: 2.minutes.ago) - allow(user).to receive(:ldap_user?).and_return(true) + context 'with an ldap user' do + let(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) } - expect { push_access_check }.not_to raise_error - end + it 'allows ldap users with expired password to push' do + expect { push_access_check }.not_to raise_error + end - it 'disallows blocked ldap users with expired password to push' do - user.block - user.update!(password_expires_at: 2.minutes.ago) - allow(user).to receive(:ldap_user?).and_return(true) + it 'disallows blocked ldap users with expired password to push' do + user.block - expect { push_access_check }.to raise_forbidden("Your account has been blocked.") + expect { push_access_check }.to raise_forbidden("Your account has been blocked.") + end end it 'cleans up the files' do diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb index 69828c143db..401ffee9c28 100644 --- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb +++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb @@ -16,6 +16,8 @@ RSpec.describe Gitlab::HealthChecks::Probes::Collection do Gitlab::HealthChecks::Redis::CacheCheck, Gitlab::HealthChecks::Redis::QueuesCheck, Gitlab::HealthChecks::Redis::SharedStateCheck, + Gitlab::HealthChecks::Redis::TraceChunksCheck, + Gitlab::HealthChecks::Redis::RateLimitingCheck, Gitlab::HealthChecks::GitalyCheck ] end diff --git a/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb b/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb new file mode 100644 index 00000000000..1521fc99cde --- /dev/null +++ b/spec/lib/gitlab/health_checks/redis/rate_limiting_check_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../simple_check_shared' + +RSpec.describe Gitlab::HealthChecks::Redis::RateLimitingCheck do + include_examples 'simple_check', 'redis_rate_limiting_ping', 'RedisRateLimiting', 'PONG' +end diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index 82f465c4f9e..518a9337826 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -445,8 +445,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do expect(@project.merge_requests.size).to eq(9) end - it 'only restores valid triggers' do - expect(@project.triggers.size).to eq(1) + it 'does not restore triggers' do + expect(@project.triggers.size).to eq(0) end it 'has the correct number of pipelines and statuses' do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index a9efa32f986..287be24d11f 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -401,15 +401,6 @@ Ci::Variable: - encrypted_value - encrypted_value_salt - encrypted_value_iv -Ci::Trigger: -- id -- token -- project_id -- created_at -- updated_at -- owner_id -- description -- ref Ci::PipelineSchedule: - id - description @@ -556,7 +547,6 @@ Project: - disable_overriding_approvers_per_merge_request - merge_requests_ff_only_enabled - issues_template -- repository_size_limit - sync_time - service_desk_enabled - last_repository_updated_at diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb index ebc2e92a0dd..0da44dfb8d8 100644 --- a/spec/lib/gitlab/instrumentation/redis_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_spec.rb @@ -76,7 +76,8 @@ RSpec.describe Gitlab::Instrumentation::Redis do details_row.merge(storage: 'Cache'), details_row.merge(storage: 'Queues'), details_row.merge(storage: 'SharedState'), - details_row.merge(storage: 'TraceChunks')) + details_row.merge(storage: 'TraceChunks'), + details_row.merge(storage: 'RateLimiting')) end end end diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb index 0929b90d1f4..83ba5858d81 100644 --- a/spec/lib/gitlab/legacy_github_import/client_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb @@ -86,6 +86,15 @@ RSpec.describe Gitlab::LegacyGithubImport::Client do it 'builds a endpoint with the given options' do expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/' end + + context 'and hostname' do + subject(:client) { described_class.new(token, host: 'https://167.99.148.217/', api_version: 'v1', hostname: 'try.gitea.io') } + + it 'builds a endpoint with the given options' do + expect(client.api.connection_options.dig(:headers, :host)).to eq 'try.gitea.io' + expect(client.api.api_endpoint).to eq 'https://167.99.148.217/api/v1/' + end + end end end diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index a8472062f03..3bc0bd385a7 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -126,7 +126,7 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do end context 'when the user password is expired' do - let(:actor) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true) } + let(:actor) { create(:user, password_expires_at: 1.minute.ago) } it 'returns false' do expect(lfs_token.token_valid?(lfs_token.token)).to be false @@ -135,12 +135,12 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do end context 'when the actor is an ldap user' do - before do - allow(actor).to receive(:ldap_user?).and_return(true) - end + let(:actor) { create(:omniauth_user, provider: 'ldap') } context 'when the user is blocked' do - let(:actor) { create(:user, :blocked) } + before do + actor.block! + end it 'returns false' do expect(lfs_token.token_valid?(lfs_token.token)).to be false @@ -148,7 +148,9 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do end context 'when the user password is expired' do - let(:actor) { create(:user, password_expires_at: 1.minute.ago) } + before do + actor.update!(password_expires_at: 1.minute.ago) + end it 'returns true' do expect(lfs_token.token_valid?(lfs_token.token)).to be true diff --git a/spec/lib/gitlab/redis/rate_limiting_spec.rb b/spec/lib/gitlab/redis/rate_limiting_spec.rb new file mode 100644 index 00000000000..f15aa71a52d --- /dev/null +++ b/spec/lib/gitlab/redis/rate_limiting_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Redis::RateLimiting do + let(:instance_specific_config_file) { "config/redis.rate_limiting.yml" } + let(:environment_config_file_name) { "GITLAB_REDIS_RATE_LIMITING_CONFIG_FILE" } + let(:cache_config_file) { nil } + + before do + allow(Gitlab::Redis::Cache).to receive(:config_file_name).and_return(cache_config_file) + end + + include_examples "redis_shared_examples" + + describe '.config_file_name' do + subject { described_class.config_file_name } + + let(:rails_root) { Dir.mktmpdir('redis_shared_examples') } + + before do + # Undo top-level stub of config_file_name because we are testing that method now. + allow(described_class).to receive(:config_file_name).and_call_original + + allow(described_class).to receive(:rails_root).and_return(rails_root) + FileUtils.mkdir_p(File.join(rails_root, 'config')) + end + + after do + FileUtils.rm_rf(rails_root) + end + + context 'when there is only a resque.yml' do + before do + FileUtils.touch(File.join(rails_root, 'config/resque.yml')) + end + + it { expect(subject).to eq("#{rails_root}/config/resque.yml") } + + context 'and there is a global env override' do + before do + stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override') + end + + it { expect(subject).to eq('global override') } + + context 'and Cache has a different config file' do + let(:cache_config_file) { 'cache config file' } + + it { expect(subject).to eq('cache config file') } + end + end + end + end +end diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb index a02be83558c..0cbe44eacf4 100644 --- a/spec/lib/gitlab/string_regex_marker_spec.rb +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -23,9 +23,10 @@ RSpec.describe Gitlab::StringRegexMarker do context 'with multiple occurrences' do let(:raw) { %{a <b> <c> d} } let(:rich) { %{a <b> <c> d}.html_safe } + let(:regexp) { /<[a-z]>/ } subject do - described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:, mode:| + described_class.new(raw, rich).mark(regexp) do |text, left:, right:, mode:| %{<strong>#{text}</strong>}.html_safe end end @@ -34,6 +35,15 @@ RSpec.describe Gitlab::StringRegexMarker do expect(subject).to eq(%{a <strong><b></strong> <strong><c></strong> d}) expect(subject).to be_html_safe end + + context 'with a Gitlab::UntrustedRegexp' do + let(:regexp) { Gitlab::UntrustedRegexp.new('<[a-z]>') } + + it 'marks the matches' do + expect(subject).to eq(%{a <strong><b></strong> <strong><c></strong> d}) + expect(subject).to be_html_safe + end + end end end end diff --git a/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb new file mode 100644 index 00000000000..0d0f6a3df67 --- /dev/null +++ b/spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('cleanup_orphan_project_access_tokens') + +RSpec.describe CleanupOrphanProjectAccessTokens, :migration do + def create_user(**extra_options) + defaults = { state: 'active', projects_limit: 0, email: "#{extra_options[:username]}@example.com" } + + table(:users).create!(defaults.merge(extra_options)) + end + + def create_membership(**extra_options) + defaults = { access_level: 30, notification_level: 0, source_id: 1, source_type: 'Project' } + + table(:members).create!(defaults.merge(extra_options)) + end + + let!(:regular_user) { create_user(username: 'regular') } + let!(:orphan_bot) { create_user(username: 'orphaned_bot', user_type: 6) } + let!(:used_bot) do + create_user(username: 'used_bot', user_type: 6).tap do |bot| + create_membership(user_id: bot.id) + end + end + + it 'marks all bots without memberships as deactivated' do + expect do + migrate! + regular_user.reload + orphan_bot.reload + used_bot.reload + end.to change { + [regular_user.state, orphan_bot.state, used_bot.state] + }.from(%w[active active active]).to(%w[active deactivated active]) + end + + it 'schedules for deletion all bots without memberships' do + job_class = 'DeleteUserWorker'.safe_constantize + + if job_class + expect(job_class).to receive(:bulk_perform_async).with([[orphan_bot.id, orphan_bot.id, skip_authorization: true]]) + + migrate! + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d25011059b2..1ef0d67958c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5419,43 +5419,11 @@ RSpec.describe User do end describe '#password_expired_if_applicable?' do - let(:user) { build(:user, password_expires_at: password_expires_at, password_automatically_set: set_automatically?) } + let(:user) { build(:user, password_expires_at: password_expires_at) } subject { user.password_expired_if_applicable? } - context 'when user is not ldap user' do - context 'when user has password set automatically' do - let(:set_automatically?) { true } - - context 'when password_expires_at is not set' do - let(:password_expires_at) {} - - it 'returns false' do - is_expected.to be_falsey - end - end - - context 'when password_expires_at is in the past' do - let(:password_expires_at) { 1.minute.ago } - - it 'returns true' do - is_expected.to be_truthy - end - end - - context 'when password_expires_at is in the future' do - let(:password_expires_at) { 1.minute.from_now } - - it 'returns false' do - is_expected.to be_falsey - end - end - end - end - - context 'when user has password not set automatically' do - let(:set_automatically?) { false } - + shared_examples 'password expired not applicable' do context 'when password_expires_at is not set' do let(:password_expires_at) {} @@ -5481,13 +5449,7 @@ RSpec.describe User do end end - context 'when user is ldap user' do - let(:user) { build(:user, password_expires_at: password_expires_at) } - - before do - allow(user).to receive(:ldap_user?).and_return(true) - end - + context 'with a regular user' do context 'when password_expires_at is not set' do let(:password_expires_at) {} @@ -5499,8 +5461,8 @@ RSpec.describe User do context 'when password_expires_at is in the past' do let(:password_expires_at) { 1.minute.ago } - it 'returns false' do - is_expected.to be_falsey + it 'returns true' do + is_expected.to be_truthy end end @@ -5513,32 +5475,26 @@ RSpec.describe User do end end - context 'when user is a project bot' do - let(:user) { build(:user, :project_bot, password_expires_at: password_expires_at) } - - context 'when password_expires_at is not set' do - let(:password_expires_at) {} - - it 'returns false' do - is_expected.to be_falsey - end + context 'when user is a bot' do + before do + allow(user).to receive(:bot?).and_return(true) end - context 'when password_expires_at is in the past' do - let(:password_expires_at) { 1.minute.ago } + it_behaves_like 'password expired not applicable' + end - it 'returns false' do - is_expected.to be_falsey - end - end + context 'when password_automatically_set is true' do + let(:user) { create(:omniauth_user, provider: 'ldap')} - context 'when password_expires_at is in the future' do - let(:password_expires_at) { 1.minute.from_now } + it_behaves_like 'password expired not applicable' + end - it 'returns false' do - is_expected.to be_falsey - end + context 'when allow_password_authentication is false' do + before do + allow(user).to receive(:allow_password_authentication?).and_return(false) end + + it_behaves_like 'password expired not applicable' end end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index 122612df355..ca9a5b1853c 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -249,15 +249,13 @@ RSpec.describe GlobalPolicy do context 'user with expired password' do before do - current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true) + current_user.update!(password_expires_at: 2.minutes.ago) end it { is_expected.not_to be_allowed(:access_api) } context 'when user is using ldap' do - before do - allow(current_user).to receive(:ldap_user?).and_return(true) - end + let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) } it { is_expected.to be_allowed(:access_api) } end @@ -445,15 +443,13 @@ RSpec.describe GlobalPolicy do context 'user with expired password' do before do - current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true) + current_user.update!(password_expires_at: 2.minutes.ago) end it { is_expected.not_to be_allowed(:access_git) } context 'when user is using ldap' do - before do - allow(current_user).to receive(:ldap_user?).and_return(true) - end + let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) } it { is_expected.to be_allowed(:access_git) } end @@ -537,15 +533,13 @@ RSpec.describe GlobalPolicy do context 'user with expired password' do before do - current_user.update!(password_expires_at: 2.minutes.ago, password_automatically_set: true) + current_user.update!(password_expires_at: 2.minutes.ago) end it { is_expected.not_to be_allowed(:use_slash_commands) } context 'when user is using ldap' do - before do - allow(current_user).to receive(:ldap_user?).and_return(true) - end + let(:current_user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) } it { is_expected.to be_allowed(:use_slash_commands) } end diff --git a/spec/requests/api/import_bitbucket_server_spec.rb b/spec/requests/api/import_bitbucket_server_spec.rb index 2225f737f36..970416c7444 100644 --- a/spec/requests/api/import_bitbucket_server_spec.rb +++ b/spec/requests/api/import_bitbucket_server_spec.rb @@ -28,6 +28,20 @@ RSpec.describe API::ImportBitbucketServer do Grape::Endpoint.before_each nil end + it 'rejects requests when Bitbucket Server Importer is disabled' do + stub_application_setting(import_sources: nil) + + post api("/import/bitbucket_server", user), params: { + bitbucket_server_url: base_uri, + bitbucket_server_username: user, + personal_access_token: token, + bitbucket_server_project: project_key, + bitbucket_server_repo: repo_slug + } + + expect(response).to have_gitlab_http_status(:forbidden) + end + it 'returns 201 response when the project is imported successfully' do allow(Gitlab::BitbucketServerImport::ProjectCreator) .to receive(:new).with(project_key, repo_slug, anything, repo_slug, user.namespace, user, anything) diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb new file mode 100644 index 00000000000..649647804c0 --- /dev/null +++ b/spec/requests/api/integrations_spec.rb @@ -0,0 +1,363 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe API::Integrations do + let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + + let_it_be(:project, reload: true) do + create(:project, creator_id: user.id, namespace: user.namespace) + end + + %w[integrations services].each do |endpoint| + describe "GET /projects/:id/#{endpoint}" do + it 'returns authentication error when unauthenticated' do + get api("/projects/#{project.id}/#{endpoint}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it "returns error when authenticated but user is not a project owner" do + project.add_developer(user2) + get api("/projects/#{project.id}/#{endpoint}", user2) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'with integrations' do + let!(:active_integration) { create(:emails_on_push_integration, project: project, active: true) } + let!(:integration) { create(:custom_issue_tracker_integration, project: project, active: false) } + + it "returns a list of all active integrations" do + get api("/projects/#{project.id}/#{endpoint}", user) + + aggregate_failures 'expect successful response with all active integrations' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['slug']).to eq('emails-on-push') + expect(response).to match_response_schema('public_api/v4/integrations') + end + end + end + end + + Integration.available_integration_names.each do |integration| + describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do + include_context integration + + it "updates #{integration} settings" do + put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: integration_attrs + + expect(response).to have_gitlab_http_status(:ok) + + current_integration = project.integrations.first + events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names + query_strings = [] + events.each do |event| + query_strings << "#{event}=#{!current_integration[event]}" + end + query_strings = query_strings.join('&') + + put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}?#{query_strings}", user), params: integration_attrs + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['slug']).to eq(dashed_integration) + events.each do |event| + next if event == "foo" + + expect(project.integrations.first[event]).not_to eq(current_integration[event]), + "expected #{!current_integration[event]} for event #{event} for #{endpoint} #{current_integration.title}, got #{current_integration[event]}" + end + end + + it "returns if required fields missing" do + required_attributes = integration_attrs_list.select do |attr| + integration_klass.validators_on(attr).any? do |v| + v.instance_of?(ActiveRecord::Validations::PresenceValidator) && + # exclude presence validators with conditional since those are not really required + ![:if, :unless].any? { |cond| v.options.include?(cond) } + end + end + + if required_attributes.empty? + expected_code = :ok + else + integration_attrs.delete(required_attributes.sample) + expected_code = :bad_request + end + + put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: integration_attrs + + expect(response).to have_gitlab_http_status(expected_code) + end + end + + describe "DELETE /projects/:id/#{endpoint}/#{integration.dasherize}" do + include_context integration + + before do + initialize_integration(integration) + end + + it "deletes #{integration}" do + delete api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(response).to have_gitlab_http_status(:no_content) + project.send(integration_method).reload + expect(project.send(integration_method).activated?).to be_falsey + end + end + + describe "GET /projects/:id/#{endpoint}/#{integration.dasherize}" do + include_context integration + + let!(:initialized_integration) { initialize_integration(integration, active: true) } + + let_it_be(:project2) do + create(:project, creator_id: user.id, namespace: user.namespace) + end + + def deactive_integration! + return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus) + + # Integrations::Prometheus sets `#active` itself within a `before_save`: + initialized_integration.manual_configuration = false + initialized_integration.save! + end + + it 'returns authentication error when unauthenticated' do + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}") + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it "returns all properties of active integration #{integration}" do + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(initialized_integration).to be_active + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) + end + + it "returns all properties of inactive integration #{integration}" do + deactive_integration! + + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(initialized_integration).not_to be_active + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) + end + + it "returns not found if integration does not exist" do + get api("/projects/#{project2.id}/#{endpoint}/#{dashed_integration}", user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Integration Not Found') + end + + it "returns not found if integration exists but is in `Project#disabled_integrations`" do + expect_next_found_instance_of(Project) do |project| + expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration]) + end + + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Integration Not Found') + end + + it "returns error when authenticated but not a project owner" do + project.add_developer(user2) + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user2) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe "POST /projects/:id/#{endpoint}/:slug/trigger" do + describe 'Mattermost integration' do + let(:integration_name) { 'mattermost_slash_commands' } + + context 'when no integration is available' do + it 'returns a not found message' do + post api("/projects/#{project.id}/#{endpoint}/idonotexist/trigger") + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response["error"]).to eq("404 Not Found") + end + end + + context 'when the integration exists' do + let(:params) { { token: 'token' } } + + context 'when the integration is not active' do + before do + project.create_mattermost_slash_commands_integration( + active: false, + properties: params + ) + end + + it 'when the integration is inactive' do + post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the integration is active' do + before do + project.create_mattermost_slash_commands_integration( + active: true, + properties: params + ) + end + + it 'returns status 200' do + post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the project can not be found' do + it 'returns a generic 404' do + post api("/projects/404/#{endpoint}/#{integration_name}/trigger"), params: params + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response["message"]).to eq("404 Integration Not Found") + end + end + end + end + + describe 'Slack Integration' do + let(:integration_name) { 'slack_slash_commands' } + + before do + project.create_slack_slash_commands_integration( + active: true, + properties: { token: 'token' } + ) + end + + it 'returns status 200' do + post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: { token: 'token', text: 'help' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['response_type']).to eq("ephemeral") + end + end + end + + describe 'Mattermost integration' do + let(:integration_name) { 'mattermost' } + let(:params) do + { webhook: 'https://hook.example.com', username: 'username' } + end + + before do + project.create_mattermost_integration( + active: true, + properties: params + ) + end + + it 'accepts a username for update' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(username: 'new_username') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['username']).to eq('new_username') + end + end + + describe 'Microsoft Teams integration' do + let(:integration_name) { 'microsoft-teams' } + let(:params) do + { + webhook: 'https://hook.example.com', + branches_to_be_notified: 'default', + notify_only_broken_pipelines: false + } + end + + before do + project.create_microsoft_teams_integration( + active: true, + properties: params + ) + end + + it 'accepts branches_to_be_notified for update' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), + params: params.merge(branches_to_be_notified: 'all') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['branches_to_be_notified']).to eq('all') + end + + it 'accepts notify_only_broken_pipelines for update' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), + params: params.merge(notify_only_broken_pipelines: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) + end + end + + describe 'Hangouts Chat integration' do + let(:integration_name) { 'hangouts-chat' } + let(:params) do + { + webhook: 'https://hook.example.com', + branches_to_be_notified: 'default' + } + end + + before do + project.create_hangouts_chat_integration( + active: true, + properties: params + ) + end + + it 'accepts branches_to_be_notified for update', :aggregate_failures do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['branches_to_be_notified']).to eq('all') + end + + it 'only requires the webhook param' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: { webhook: 'https://hook.example.com' } + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'Pipelines Email Integration' do + let(:integration_name) { 'pipelines-email' } + + context 'notify_only_broken_pipelines property was saved as a string' do + before do + project.create_pipelines_email_integration( + active: false, + properties: { + "notify_only_broken_pipelines": "true", + "branches_to_be_notified": "default" + } + ) + end + + it 'returns boolean values for notify_only_broken_pipelines' do + get api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user) + + expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) + end + end + end + end +end diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index 76a4548df8a..b23ba0021e0 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -259,22 +259,32 @@ RSpec.describe API::Invitations do let(:route) { get invitations_url(source, stranger) } end - %i[maintainer developer access_requester stranger].each do |type| + context "when authenticated as a maintainer" do + it 'returns 200' do + get invitations_url(source, maintainer) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(0) + end + end + + %i[developer access_requester stranger].each do |type| context "when authenticated as a #{type}" do - it 'returns 200' do + it 'returns 403' do user = public_send(type) get invitations_url(source, user) - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.size).to eq(0) + expect(response).to have_gitlab_http_status(:forbidden) end end end it 'avoids N+1 queries' do + invite_member_by_email(source, source_type, email, maintainer) + # Establish baseline get invitations_url(source, maintainer) @@ -282,7 +292,7 @@ RSpec.describe API::Invitations do get invitations_url(source, maintainer) end - invite_member_by_email(source, source_type, email, maintainer) + invite_member_by_email(source, source_type, email2, maintainer) expect do get invitations_url(source, maintainer) @@ -290,7 +300,7 @@ RSpec.describe API::Invitations do end it 'does not find confirmed members' do - get invitations_url(source, developer) + get invitations_url(source, maintainer) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -300,10 +310,10 @@ RSpec.describe API::Invitations do end it 'finds all members with no query string specified' do - invite_member_by_email(source, source_type, email, developer) - invite_member_by_email(source, source_type, email2, developer) + invite_member_by_email(source, source_type, email, maintainer) + invite_member_by_email(source, source_type, email2, maintainer) - get invitations_url(source, developer), params: { query: '' } + get invitations_url(source, maintainer), params: { query: '' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -314,17 +324,17 @@ RSpec.describe API::Invitations do end it 'finds the invitation by invite_email with query string' do - invite_member_by_email(source, source_type, email, developer) - invite_member_by_email(source, source_type, email2, developer) + invite_member_by_email(source, source_type, email, maintainer) + invite_member_by_email(source, source_type, email2, maintainer) - get invitations_url(source, developer), params: { query: email } + get invitations_url(source, maintainer), params: { query: email } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.count).to eq(1) expect(json_response.first['invite_email']).to eq(email) - expect(json_response.first['created_by_name']).to eq(developer.name) + expect(json_response.first['created_by_name']).to eq(maintainer.name) expect(json_response.first['user_name']).to eq(nil) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 80bccdfee0c..be8a6c7bdcf 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1149,6 +1149,16 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:bad_request) end + it 'disallows creating a project with an import_url when git import source is disabled' do + stub_application_setting(import_sources: nil) + + project_params = { import_url: 'http://example.com', path: 'path-project-Foo', name: 'Foo Project' } + expect { post api('/projects', user), params: project_params } + .not_to change { Project.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + it 'sets a project as public' do project = attributes_for(:project, visibility: 'public') diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb deleted file mode 100644 index e550132e776..00000000000 --- a/spec/requests/api/services_spec.rb +++ /dev/null @@ -1,361 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe API::Services do - let_it_be(:user) { create(:user) } - let_it_be(:user2) { create(:user) } - - let_it_be(:project, reload: true) do - create(:project, creator_id: user.id, namespace: user.namespace) - end - - describe "GET /projects/:id/services" do - it 'returns authentication error when unauthenticated' do - get api("/projects/#{project.id}/services") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it "returns error when authenticated but user is not a project owner" do - project.add_developer(user2) - get api("/projects/#{project.id}/services", user2) - - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'with integrations' do - let!(:active_integration) { create(:emails_on_push_integration, project: project, active: true) } - let!(:integration) { create(:custom_issue_tracker_integration, project: project, active: false) } - - it "returns a list of all active integrations" do - get api("/projects/#{project.id}/services", user) - - aggregate_failures 'expect successful response with all active integrations' do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an Array - expect(json_response.count).to eq(1) - expect(json_response.first['slug']).to eq('emails-on-push') - expect(response).to match_response_schema('public_api/v4/services') - end - end - end - end - - Integration.available_integration_names.each do |integration| - describe "PUT /projects/:id/services/#{integration.dasherize}" do - include_context integration - - it "updates #{integration} settings" do - put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs - - expect(response).to have_gitlab_http_status(:ok) - - current_integration = project.integrations.first - events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names - query_strings = [] - events.each do |event| - query_strings << "#{event}=#{!current_integration[event]}" - end - query_strings = query_strings.join('&') - - put api("/projects/#{project.id}/services/#{dashed_integration}?#{query_strings}", user), params: integration_attrs - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['slug']).to eq(dashed_integration) - events.each do |event| - next if event == "foo" - - expect(project.integrations.first[event]).not_to eq(current_integration[event]), - "expected #{!current_integration[event]} for event #{event} for service #{current_integration.title}, got #{current_integration[event]}" - end - end - - it "returns if required fields missing" do - required_attributes = integration_attrs_list.select do |attr| - integration_klass.validators_on(attr).any? do |v| - v.instance_of?(ActiveRecord::Validations::PresenceValidator) && - # exclude presence validators with conditional since those are not really required - ![:if, :unless].any? { |cond| v.options.include?(cond) } - end - end - - if required_attributes.empty? - expected_code = :ok - else - integration_attrs.delete(required_attributes.sample) - expected_code = :bad_request - end - - put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs - - expect(response).to have_gitlab_http_status(expected_code) - end - end - - describe "DELETE /projects/:id/services/#{integration.dasherize}" do - include_context integration - - before do - initialize_integration(integration) - end - - it "deletes #{integration}" do - delete api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(response).to have_gitlab_http_status(:no_content) - project.send(integration_method).reload - expect(project.send(integration_method).activated?).to be_falsey - end - end - - describe "GET /projects/:id/services/#{integration.dasherize}" do - include_context integration - - let!(:initialized_integration) { initialize_integration(integration, active: true) } - - let_it_be(:project2) do - create(:project, creator_id: user.id, namespace: user.namespace) - end - - def deactive_integration! - return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus) - - # Integrations::Prometheus sets `#active` itself within a `before_save`: - initialized_integration.manual_configuration = false - initialized_integration.save! - end - - it 'returns authentication error when unauthenticated' do - get api("/projects/#{project.id}/services/#{dashed_integration}") - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it "returns all properties of active service #{integration}" do - get api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(initialized_integration).to be_active - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) - end - - it "returns all properties of inactive integration #{integration}" do - deactive_integration! - - get api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(initialized_integration).not_to be_active - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) - end - - it "returns not found if integration does not exist" do - get api("/projects/#{project2.id}/services/#{dashed_integration}", user) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 Service Not Found') - end - - it "returns not found if service exists but is in `Project#disabled_integrations`" do - expect_next_found_instance_of(Project) do |project| - expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration]) - end - - get api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 Service Not Found') - end - - it "returns error when authenticated but not a project owner" do - project.add_developer(user2) - get api("/projects/#{project.id}/services/#{dashed_integration}", user2) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - describe 'POST /projects/:id/services/:slug/trigger' do - describe 'Mattermost integration' do - let(:integration_name) { 'mattermost_slash_commands' } - - context 'when no integration is available' do - it 'returns a not found message' do - post api("/projects/#{project.id}/services/idonotexist/trigger") - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response["error"]).to eq("404 Not Found") - end - end - - context 'when the integration exists' do - let(:params) { { token: 'token' } } - - context 'when the integration is not active' do - before do - project.create_mattermost_slash_commands_integration( - active: false, - properties: params - ) - end - - it 'when the integration is inactive' do - post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when the integration is active' do - before do - project.create_mattermost_slash_commands_integration( - active: true, - properties: params - ) - end - - it 'returns status 200' do - post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: params - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when the project can not be found' do - it 'returns a generic 404' do - post api("/projects/404/services/#{integration_name}/trigger"), params: params - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response["message"]).to eq("404 Service Not Found") - end - end - end - end - - describe 'Slack Integration' do - let(:integration_name) { 'slack_slash_commands' } - - before do - project.create_slack_slash_commands_integration( - active: true, - properties: { token: 'token' } - ) - end - - it 'returns status 200' do - post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: { token: 'token', text: 'help' } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['response_type']).to eq("ephemeral") - end - end - end - - describe 'Mattermost integration' do - let(:integration_name) { 'mattermost' } - let(:params) do - { webhook: 'https://hook.example.com', username: 'username' } - end - - before do - project.create_mattermost_integration( - active: true, - properties: params - ) - end - - it 'accepts a username for update' do - put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(username: 'new_username') - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['username']).to eq('new_username') - end - end - - describe 'Microsoft Teams integration' do - let(:integration_name) { 'microsoft-teams' } - let(:params) do - { - webhook: 'https://hook.example.com', - branches_to_be_notified: 'default', - notify_only_broken_pipelines: false - } - end - - before do - project.create_microsoft_teams_integration( - active: true, - properties: params - ) - end - - it 'accepts branches_to_be_notified for update' do - put api("/projects/#{project.id}/services/#{integration_name}", user), - params: params.merge(branches_to_be_notified: 'all') - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['branches_to_be_notified']).to eq('all') - end - - it 'accepts notify_only_broken_pipelines for update' do - put api("/projects/#{project.id}/services/#{integration_name}", user), - params: params.merge(notify_only_broken_pipelines: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) - end - end - - describe 'Hangouts Chat integration' do - let(:integration_name) { 'hangouts-chat' } - let(:params) do - { - webhook: 'https://hook.example.com', - branches_to_be_notified: 'default' - } - end - - before do - project.create_hangouts_chat_integration( - active: true, - properties: params - ) - end - - it 'accepts branches_to_be_notified for update', :aggregate_failures do - put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all') - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['branches_to_be_notified']).to eq('all') - end - - it 'only requires the webhook param' do - put api("/projects/#{project.id}/services/#{integration_name}", user), params: { webhook: 'https://hook.example.com' } - - expect(response).to have_gitlab_http_status(:ok) - end - end - - describe 'Pipelines Email Integration' do - let(:integration_name) { 'pipelines-email' } - - context 'notify_only_broken_pipelines property was saved as a string' do - before do - project.create_pipelines_email_integration( - active: false, - properties: { - "notify_only_broken_pipelines": "true", - "branches_to_be_notified": "default" - } - ) - end - - it 'returns boolean values for notify_only_broken_pipelines' do - get api("/projects/#{project.id}/services/#{integration_name}", user) - - expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) - end - end - end -end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 527e548ad19..ee1911b0a26 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -53,37 +53,6 @@ RSpec.describe API::Users do end end - describe 'GET /users/:id' do - context 'when unauthenticated' do - it 'does not contain the note of the user' do - get api("/users/#{user.id}") - - expect(json_response).not_to have_key('note') - end - end - - context 'when authenticated' do - context 'as an admin' do - it 'contains the note of the user' do - get api("/users/#{user.id}", admin) - - expect(json_response).to have_key('note') - expect(json_response['note']).to eq(user.note) - expect(json_response).to have_key('sign_in_count') - end - end - - context 'as a regular user' do - it 'does not contain the note of the user' do - get api("/users/#{user.id}", user) - - expect(json_response).not_to have_key('note') - expect(json_response).not_to have_key('sign_in_count') - end - end - end - end - describe "PUT /users/:id" do context 'when user is an admin' do it "updates note of the user" do @@ -527,6 +496,8 @@ RSpec.describe API::Users do end describe "GET /users/:id" do + let_it_be(:user2, reload: true) { create(:user, username: 'another_user') } + it "returns a user by id" do get api("/users/#{user.id}", user) @@ -564,6 +535,64 @@ RSpec.describe API::Users do expect(json_response.keys).not_to include 'trial' end + it 'returns a 404 if the target user is present but inaccessible' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_user, user2).and_return(false) + + get api("/users/#{user2.id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns the `created_at` field for public users' do + get api("/users/#{user2.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response.keys).to include('created_at') + end + + it 'does not return the `created_at` field for private users' do + get api("/users/#{private_user.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response.keys).not_to include('created_at') + end + + it 'returns the `followers` field for public users' do + get api("/users/#{user2.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response.keys).to include('followers') + end + + it 'does not return the `followers` field for private users' do + get api("/users/#{private_user.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response.keys).not_to include('followers') + end + + it 'returns the `following` field for public users' do + get api("/users/#{user2.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response.keys).to include('following') + end + + it 'does not return the `following` field for private users' do + get api("/users/#{private_user.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response.keys).not_to include('following') + end + + it 'does not contain the note of the user' do + get api("/users/#{user.id}", user) + + expect(json_response).not_to have_key('note') + expect(json_response).not_to have_key('sign_in_count') + end + context 'when job title is present' do let(:job_title) { 'Fullstack Engineer' } @@ -580,6 +609,14 @@ RSpec.describe API::Users do end context 'when authenticated as admin' do + it 'contains the note of the user' do + get api("/users/#{user.id}", admin) + + expect(json_response).to have_key('note') + expect(json_response['note']).to eq(user.note) + expect(json_response).to have_key('sign_in_count') + end + it 'includes the `is_admin` field' do get api("/users/#{user.id}", admin) @@ -640,62 +677,10 @@ RSpec.describe API::Users do end context 'for an anonymous user' do - it "returns a user by id" do - get api("/users/#{user.id}") - - expect(response).to match_response_schema('public_api/v4/user/basic') - expect(json_response['username']).to eq(user.username) - end - - it "returns a 404 if the target user is present but inaccessible" do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false) - - get api("/users/#{user.id}") - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns the `created_at` field for public users" do - get api("/users/#{user.id}") - - expect(response).to match_response_schema('public_api/v4/user/basic') - expect(json_response.keys).to include 'created_at' - end - - it "does not return the `created_at` field for private users" do - get api("/users/#{private_user.id}") - - expect(response).to match_response_schema('public_api/v4/user/basic') - expect(json_response.keys).not_to include 'created_at' - end - - it "returns the `followers` field for public users" do - get api("/users/#{user.id}") - - expect(response).to match_response_schema('public_api/v4/user/basic') - expect(json_response.keys).to include 'followers' - end - - it "does not return the `followers` field for private users" do - get api("/users/#{private_user.id}") - - expect(response).to match_response_schema('public_api/v4/user/basic') - expect(json_response.keys).not_to include 'followers' - end - - it "returns the `following` field for public users" do + it 'returns 403' do get api("/users/#{user.id}") - expect(response).to match_response_schema('public_api/v4/user/basic') - expect(json_response.keys).to include 'following' - end - - it "does not return the `following` field for private users" do - get api("/users/#{private_user.id}") - - expect(response).to match_response_schema('public_api/v4/user/basic') - expect(json_response.keys).not_to include 'following' + expect(response).to have_gitlab_http_status(:forbidden) end end @@ -788,6 +773,14 @@ RSpec.describe API::Users do describe 'GET /users/:id/followers' do let(:follower) { create(:user) } + context 'for an anonymous user' do + it 'returns 403' do + get api("/users/#{user.id}") + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + context 'user has followers' do it 'lists followers' do follower.follow(user) @@ -823,6 +816,14 @@ RSpec.describe API::Users do describe 'GET /users/:id/following' do let(:followee) { create(:user) } + context 'for an anonymous user' do + it 'returns 403' do + get api("/users/#{user.id}") + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + context 'user has followers' do it 'lists following user' do user.follow(followee) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index a16f5abf608..d2528600477 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -61,7 +61,7 @@ RSpec.describe 'Git HTTP requests' do shared_examples 'operations are not allowed with expired password' do context "when password is expired" do it "responds to downloads with status 401 Unauthorized" do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) download(path, user: user.username, password: user.password) do |response| expect(response).to have_gitlab_http_status(:unauthorized) @@ -69,7 +69,7 @@ RSpec.describe 'Git HTTP requests' do end it "responds to uploads with status 401 Unauthorized" do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) upload(path, user: user.username, password: user.password) do |response| expect(response).to have_gitlab_http_status(:unauthorized) @@ -614,7 +614,7 @@ RSpec.describe 'Git HTTP requests' do context "when password is expired" do it "responds to downloads with status 401 unauthorized" do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) download(path, **env) do |response| expect(response).to have_gitlab_http_status(:unauthorized) @@ -697,7 +697,7 @@ RSpec.describe 'Git HTTP requests' do context "when password is expired" do it "responds to uploads with status 401 unauthorized" do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository]) @@ -950,7 +950,7 @@ RSpec.describe 'Git HTTP requests' do context 'when users password is expired' do it 'rejects pulls with 401 unauthorized' do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) download(path, user: 'gitlab-ci-token', password: build.token) do |response| expect(response).to have_gitlab_http_status(:unauthorized) @@ -1245,7 +1245,7 @@ RSpec.describe 'Git HTTP requests' do context "when password is expired" do it "responds to downloads with status 401 unauthorized" do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) download(path, **env) do |response| expect(response).to have_gitlab_http_status(:unauthorized) @@ -1328,7 +1328,7 @@ RSpec.describe 'Git HTTP requests' do context "when password is expired" do it "responds to uploads with status 401 unauthorized" do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository]) @@ -1555,7 +1555,7 @@ RSpec.describe 'Git HTTP requests' do context 'when users password is expired' do it 'rejects pulls with 401 unauthorized' do - user.update!(password_expires_at: 2.days.ago, password_automatically_set: true) + user.update!(password_expires_at: 2.days.ago) download(path, user: 'gitlab-ci-token', password: build.token) do |response| expect(response).to have_gitlab_http_status(:unauthorized) diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 02eb4262690..656ae744ac1 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -126,7 +126,7 @@ RSpec.describe 'Git LFS API and storage' do it_behaves_like 'LFS http 200 blob response' context 'when user password is expired' do - let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true)} + let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)} it_behaves_like 'LFS http 401 response' end @@ -344,7 +344,7 @@ RSpec.describe 'Git LFS API and storage' do end context 'when user password is expired' do - let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true)} + let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)} let(:role) { :reporter} @@ -958,7 +958,7 @@ RSpec.describe 'Git LFS API and storage' do it_behaves_like 'LFS http 200 workhorse response' context 'when user password is expired' do - let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago, password_automatically_set: true) } + let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) } it_behaves_like 'LFS http 401 response' end diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb index 904bfd3e7c3..6491c9ab65a 100644 --- a/spec/requests/rack_attack_global_spec.rb +++ b/spec/requests/rack_attack_global_spec.rb @@ -1144,17 +1144,28 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac end context 'authenticated with lfs token' do - it 'request is authenticated by token in basic auth' do - lfs_token = Gitlab::LfsToken.new(user) - encoded_login = ["#{user.username}:#{lfs_token.token}"].pack('m0') + let(:lfs_url) { '/namespace/repo.git/info/lfs/objects/batch' } + let(:lfs_token) { Gitlab::LfsToken.new(user) } + let(:encoded_login) { ["#{user.username}:#{lfs_token.token}"].pack('m0') } + let(:headers) { { 'AUTHORIZATION' => "Basic #{encoded_login}" } } + it 'request is authenticated by token in basic auth' do expect_authenticated_request - get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" } + get lfs_url, headers: headers + end + + it 'request is not authenticated with API URL' do + expect_unauthenticated_request + + get url, headers: headers end end context 'authenticated with regular login' do + let(:encoded_login) { ["#{user.username}:#{user.password}"].pack('m0') } + let(:headers) { { 'AUTHORIZATION' => "Basic #{encoded_login}" } } + it 'request is authenticated after login' do login_as(user) @@ -1163,12 +1174,30 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac get url end - it 'request is authenticated by credentials in basic auth' do - encoded_login = ["#{user.username}:#{user.password}"].pack('m0') + it 'request is not authenticated by credentials in basic auth' do + expect_unauthenticated_request - expect_authenticated_request + get url, headers: headers + end + + context 'with POST git-upload-pack' do + it 'request is authenticated by credentials in basic auth' do + expect(::Gitlab::Workhorse).to receive(:verify_api_request!) + + expect_authenticated_request - get url, headers: { 'AUTHORIZATION' => "Basic #{encoded_login}" } + post '/namespace/repo.git/git-upload-pack', headers: headers + end + end + + context 'with GET info/refs' do + it 'request is authenticated by credentials in basic auth' do + expect(::Gitlab::Workhorse).to receive(:verify_api_request!) + + expect_authenticated_request + + get '/namespace/repo.git/info/refs?service=git-upload-pack', headers: headers + end end end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index d09c9e692de..3fe585e87bb 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -452,6 +452,16 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do end end + context 'when project has project bots' do + let!(:project_bot) { create(:user, :project_bot).tap { |user| project.add_maintainer(user) } } + + it 'deletes bot user as well' do + expect do + destroy_project(project, user) + end.to change { User.find_by(id: project_bot.id) }.to(nil) + end + end + context 'error while destroying', :sidekiq_inline do let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) } diff --git a/spec/support/redis.rb b/spec/support/redis.rb index eeeb93fa811..946c8685741 100644 --- a/spec/support/redis.rb +++ b/spec/support/redis.rb @@ -38,4 +38,12 @@ RSpec.configure do |config| redis_trace_chunks_cleanup! end + + config.around(:each, :clean_gitlab_redis_rate_limiting) do |example| + redis_rate_limiting_cleanup! + + example.run + + redis_rate_limiting_cleanup! + end end diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb index 3511d906203..bf52da5d6f2 100644 --- a/spec/support/redis/redis_helpers.rb +++ b/spec/support/redis/redis_helpers.rb @@ -22,4 +22,9 @@ module RedisHelpers def redis_trace_chunks_cleanup! Gitlab::Redis::TraceChunks.with(&:flushdb) end + + # Usage: rate limiting state (for Rack::Attack) + def redis_rate_limiting_cleanup! + Gitlab::Redis::RateLimiting.with(&:flushdb) + end end diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index 25eab5fd6e4..09bb3363205 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -327,6 +327,12 @@ RSpec.shared_examples "redis_shared_examples" do expect(subject.send(:fetch_config)).to eq false end + + it 'has a value for the legacy default URL' do + allow(subject).to receive(:fetch_config) { false } + + expect(subject.send(:raw_config_hash)).to include(url: a_string_matching(%r{\Aredis://localhost:638[012]\Z})) + end end def clear_raw_config diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index a20c1d78912..62b35923bcd 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -323,6 +323,16 @@ RSpec.shared_examples 'handle uploads authorize' do end end + context 'when id is not passed as a param' do + let(:params) { super().without(:id) } + + it 'returns 404 status' do + post_authorize + + expect(response).to have_gitlab_http_status(:not_found) + end + end + context 'when a user can upload a file' do before do sign_in(user) |