diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/support/shared_examples/requests | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) | |
download | gitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/support/shared_examples/requests')
11 files changed, 1142 insertions, 21 deletions
diff --git a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb index 9bfd1e6faa0..e94d29febfb 100644 --- a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -# Shared examples to that test code that creates AwardEmoji also mark Todos -# as done. +# Shared examples to test that the code that creates AwardEmoji also marks +# ToDos as done. # # The examples expect these to be defined in the calling spec: # - `subject` the callable code that executes the creation of an AwardEmoji # - `user` # - `project` +# RSpec.shared_examples 'creating award emojis marks Todos as done' do using RSpec::Parameterized::TableSyntax @@ -22,7 +23,7 @@ RSpec.shared_examples 'creating award emojis marks Todos as done' do with_them do let(:project) { awardable.project } - let(:awardable) { create(type) } + let(:awardable) { create(type) } # rubocop:disable Rails/SaveBang let!(:todo) { create(:todo, target: awardable, project: project, user: user) } specify do diff --git a/spec/support/shared_examples/requests/api/boards_shared_examples.rb b/spec/support/shared_examples/requests/api/boards_shared_examples.rb index 20b0f4f0dd2..0096aab55e3 100644 --- a/spec/support/shared_examples/requests/api/boards_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/boards_shared_examples.rb @@ -169,7 +169,7 @@ RSpec.shared_examples 'group and project boards' do |route_definition, ee = fals before do if board_parent.try(:namespace) - board_parent.update(namespace: owner.namespace) + board_parent.update!(namespace: owner.namespace) else board.resource_parent.add_owner(owner) end diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb index 09743c20fba..5c122b4b5d6 100644 --- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb @@ -16,8 +16,11 @@ RSpec.shared_examples 'Composer package index' do |user_type, status, add_member subject expect(response).to have_gitlab_http_status(status) - expect(response).to match_response_schema('public_api/v4/packages/composer/index') - expect(json_response).to eq presenter.root + + if status == :success + expect(response).to match_response_schema('public_api/v4/packages/composer/index') + expect(json_response).to eq presenter.root + end end end end @@ -87,13 +90,22 @@ RSpec.shared_examples 'process Composer api request' do |user_type, status, add_ end end -RSpec.shared_context 'Composer auth headers' do |user_role, user_token| +RSpec.shared_context 'Composer auth headers' do |user_role, user_token, auth_method = :token| let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + + let(:headers) do + if user_role == :anonymous + {} + elsif auth_method == :token + { 'Private-Token' => token } + else + basic_auth_header(user.username, token) + end + end end -RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token| - include_context 'Composer auth headers', user_role, user_token do +RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token, auth_method| + include_context 'Composer auth headers', user_role, user_token, auth_method do before do project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) end diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb new file mode 100644 index 00000000000..c56290a0aa9 --- /dev/null +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -0,0 +1,843 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'conan ping endpoint' do + it 'responds with 401 Unauthorized when no token provided' do + get api(url) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 200 OK when valid token is provided' do + jwt = build_jwt(personal_access_token) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['X-Conan-Server-Capabilities']).to eq("") + end + + it 'responds with 200 OK when valid job token is provided' do + jwt = build_jwt_from_job(job) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['X-Conan-Server-Capabilities']).to eq("") + end + + it 'responds with 200 OK when valid deploy token is provided' do + jwt = build_jwt_from_deploy_token(deploy_token) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['X-Conan-Server-Capabilities']).to eq("") + end + + it 'responds with 401 Unauthorized when invalid access token ID is provided' do + jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when invalid user is provided' do + jwt = build_jwt(personal_access_token, user_id: 12345) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do + jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32)) + get api(url), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when invalid JWT is provided' do + get api(url), headers: build_token_auth_header('invalid-jwt') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + context 'packages feature disabled' do + it 'responds with 404 Not Found' do + stub_packages_setting(enabled: false) + get api(url) + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end + +RSpec.shared_examples 'conan search endpoint' do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + + get api(url), headers: headers, params: params + end + + subject { json_response['results'] } + + context 'returns packages with a matching name' do + let(:params) { { q: package.conan_recipe } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + + context 'returns packages using a * wildcard' do + let(:params) { { q: "#{package.name[0, 3]}*" } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + + context 'does not return non-matching packages' do + let(:params) { { q: "foo" } } + + it { is_expected.to be_blank } + end +end + +RSpec.shared_examples 'conan authenticate endpoint' do + subject { get api(url), headers: headers } + + context 'when using invalid token' do + let(:auth_token) { 'invalid_token' } + + it 'responds with 401' do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when valid JWT access token is provided' do + it 'responds with 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'token has valid validity time' do + freeze_time do + subject + + payload = JSONWebToken::HMACToken.decode( + response.body, jwt_secret).first + expect(payload['access_token']).to eq(personal_access_token.id) + expect(payload['user_id']).to eq(personal_access_token.user_id) + + duration = payload['exp'] - payload['iat'] + expect(duration).to eq(1.hour) + end + end + end + + context 'with valid job token' do + let(:auth_token) { job_token } + + it 'responds with 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with valid deploy token' do + let(:auth_token) { deploy_token.token } + + it 'responds with 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end +end + +RSpec.shared_examples 'conan check_credentials endpoint' do + it 'responds with a 200 OK with PAT' do + get api(url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'with job token' do + let(:auth_token) { job_token } + + it 'responds with a 200 OK with job token' do + get api(url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with deploy token' do + let(:auth_token) { deploy_token.token } + + it 'responds with a 200 OK with job token' do + get api(url), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it 'responds with a 401 Unauthorized when an invalid token is used' do + get api(url), headers: build_token_auth_header('invalid-token') + + expect(response).to have_gitlab_http_status(:unauthorized) + end +end + +RSpec.shared_examples 'rejects invalid recipe' do + context 'with invalid recipe path' do + let(:recipe_path) { '../../foo++../..' } + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end +end + +RSpec.shared_examples 'rejects invalid file_name' do |invalid_file_name| + let(:file_name) { invalid_file_name } + + context 'with invalid file_name' do + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end +end + +RSpec.shared_examples 'rejects recipe for invalid project' do + context 'with invalid project' do + let(:recipe_path) { 'aa/bb/cc/dd' } + let(:project_id) { 9999 } + + it_behaves_like 'not found request' + end +end + +RSpec.shared_examples 'empty recipe for not found package' do + context 'with invalid recipe url' do + let(:recipe_path) do + 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } + end + + it 'returns not found' do + allow(::Packages::Conan::PackagePresenter).to receive(:new) + .with( + nil, + user, + project, + any_args + ).and_return(presenter) + allow(presenter).to receive(:recipe_snapshot) { {} } + allow(presenter).to receive(:package_snapshot) { {} } + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq("{}") + end + end +end + +RSpec.shared_examples 'not selecting a package with the wrong type' do + context 'with a nuget package with same name and version' do + let(:conan_username) { ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } + let(:wrong_package) { create(:nuget_package, name: "wrong", version: '1.0.0', project: project) } + let(:recipe_path) { "#{wrong_package.name}/#{wrong_package.version}/#{conan_username}/foo" } + + it 'calls the presenter with a nil package' do + expect(::Packages::Conan::PackagePresenter).to receive(:new) + .with(nil, user, project, any_args) + + subject + end + end +end + +RSpec.shared_examples 'recipe download_urls' do + let(:recipe_path) { package.conan_recipe_path } + + it 'returns the download_urls for the recipe files' do + expected_response = { + 'conanfile.py' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py", + 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + allow(presenter).to receive(:recipe_urls) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + + it_behaves_like 'not selecting a package with the wrong type' +end + +RSpec.shared_examples 'package download_urls' do + let(:recipe_path) { package.conan_recipe_path } + + it 'returns the download_urls for the package files' do + expected_response = { + 'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt", + 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt", + 'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" + } + + allow(presenter).to receive(:package_urls) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + + it_behaves_like 'not selecting a package with the wrong type' +end + +RSpec.shared_examples 'rejects invalid upload_url params' do + context 'with unaccepted json format' do + let(:params) { %w[foo bar] } + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end +end + +RSpec.shared_examples 'successful response when using Unicorn' do + context 'on Unicorn', :unicorn do + it 'returns successfully' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end +end + +RSpec.shared_examples 'recipe snapshot endpoint' do + subject { get api(url), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'empty recipe for not found package' + + context 'with existing package' do + it 'returns a hash of files with their md5 hashes' do + expected_response = { + 'conanfile.py' => 'md5hash1', + 'conanmanifest.txt' => 'md5hash2' + } + + allow(presenter).to receive(:recipe_snapshot) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + end +end + +RSpec.shared_examples 'package snapshot endpoint' do + subject { get api(url), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'empty recipe for not found package' + + context 'with existing package' do + it 'returns a hash of md5 values for the files' do + expected_response = { + 'conaninfo.txt' => "md5hash1", + 'conanmanifest.txt' => "md5hash2", + 'conan_package.tgz' => "md5hash3" + } + + allow(presenter).to receive(:package_snapshot) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + end +end + +RSpec.shared_examples 'recipe download_urls endpoint' do + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'recipe download_urls' +end + +RSpec.shared_examples 'package download_urls endpoint' do + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'package download_urls' +end + +RSpec.shared_examples 'recipe upload_urls endpoint' do + let(:recipe_path) { package.conan_recipe_path } + + let(:params) do + { 'conanfile.py': 24, + 'conanmanifest.txt': 123 } + end + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects invalid upload_url params' + it_behaves_like 'successful response when using Unicorn' + + it 'returns a set of upload urls for the files requested' do + subject + + expected_response = { + 'conanfile.py': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py", + 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + expect(response.body).to eq(expected_response.to_json) + end + + context 'with conan_sources and conan_export files' do + let(:params) do + { 'conan_sources.tgz': 345, + 'conan_export.tgz': 234, + 'conanmanifest.txt': 123 } + end + + it 'returns upload urls for the additional files' do + subject + + expected_response = { + 'conan_sources.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz", + 'conan_export.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz", + 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + expect(response.body).to eq(expected_response.to_json) + end + end + + context 'with an invalid file' do + let(:params) do + { 'invalid_file.txt': 10, + 'conanmanifest.txt': 123 } + end + + it 'does not return the invalid file as an upload_url' do + subject + + expected_response = { + 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + expect(response.body).to eq(expected_response.to_json) + end + end +end + +RSpec.shared_examples 'package upload_urls endpoint' do + let(:recipe_path) { package.conan_recipe_path } + + let(:params) do + { 'conaninfo.txt': 24, + 'conanmanifest.txt': 123, + 'conan_package.tgz': 523 } + end + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects invalid upload_url params' + it_behaves_like 'successful response when using Unicorn' + + it 'returns a set of upload urls for the files requested' do + expected_response = { + 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt", + 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt", + 'conan_package.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" + } + + subject + + expect(response.body).to eq(expected_response.to_json) + end + + context 'with invalid files' do + let(:params) do + { 'conaninfo.txt': 24, + 'invalid_file.txt': 10 } + end + + it 'returns upload urls only for the valid requested files' do + expected_response = { + 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt" + } + + subject + + expect(response.body).to eq(expected_response.to_json) + end + end +end + +RSpec.shared_examples 'delete package endpoint' do + let(:recipe_path) { package.conan_recipe_path } + + it_behaves_like 'rejects invalid recipe' + + it 'returns unauthorized for users without valid permission' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'with delete permissions' do + before do + project.add_maintainer(user) + end + + it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'delete_package' + + it 'deletes a package' do + expect { subject }.to change { Packages::Package.count }.from(2).to(1) + end + end +end + +RSpec.shared_examples 'denies download with no token' do + context 'with no private token' do + let(:headers) { {} } + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end +end + +RSpec.shared_examples 'a public project with packages' do + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end +end + +RSpec.shared_examples 'an internal project with packages' do + before do + project.team.truncate + project.update_column(:visibility_level, Gitlab::VisibilityLevel::INTERNAL) + end + + it_behaves_like 'denies download with no token' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end +end + +RSpec.shared_examples 'a private project with packages' do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'denies download with no token' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download when not enough permissions' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end +end + +RSpec.shared_examples 'not found request' do + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end +end + +RSpec.shared_examples 'recipe file download endpoint' do + it_behaves_like 'a public project with packages' + it_behaves_like 'an internal project with packages' + it_behaves_like 'a private project with packages' +end + +RSpec.shared_examples 'package file download endpoint' do + it_behaves_like 'a public project with packages' + it_behaves_like 'an internal project with packages' + it_behaves_like 'a private project with packages' + + context 'tracking the conan_package.tgz download' do + let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) } + + it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'pull_package' + end +end + +RSpec.shared_examples 'project not found by recipe' do + let(:recipe_path) { 'not/package/for/project' } + + it_behaves_like 'not found request' +end + +RSpec.shared_examples 'project not found by project id' do + let(:project_id) { 99999 } + + it_behaves_like 'not found request' +end + +RSpec.shared_examples 'workhorse authorize endpoint' do + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' + it_behaves_like 'workhorse authorization' +end + +RSpec.shared_examples 'workhorse recipe file upload endpoint' do + let(:file_name) { 'conanfile.py' } + let(:params) { { file: temp_file(file_name) } } + + subject do + workhorse_finalize( + url, + method: :put, + file_key: :file, + params: params, + headers: headers_with_token, + send_rewritten_field: true + ) + end + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' + it_behaves_like 'uploads a package file' +end + +RSpec.shared_examples 'workhorse package file upload endpoint' do + let(:file_name) { 'conaninfo.txt' } + let(:params) { { file: temp_file(file_name) } } + + subject do + workhorse_finalize( + url, + method: :put, + file_key: :file, + params: params, + headers: headers_with_token, + send_rewritten_field: true + ) + end + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest' + it_behaves_like 'uploads a package file' + + context 'tracking the conan_package.tgz upload' do + let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } + + it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'push_package' + end +end + +RSpec.shared_examples 'uploads a package file' do + context 'file size above maximum limit' do + before do + params['file.size'] = project.actual_limits.conan_max_file_size + 1 + end + + it 'handles as a local file' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'with object storage disabled' do + context 'without a file from workhorse' do + let(:params) { { file: nil } } + + it 'rejects the request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'with a file' do + it_behaves_like 'package workhorse uploads' + end + + context 'without a token' do + it 'rejects request without a token' do + headers_with_token.delete('HTTP_AUTHORIZATION') + + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when params from workhorse are correct' do + it 'creates package and stores package file' do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq(params[:file].original_filename) + end + + it "doesn't attempt to migrate file to object storage" do + expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async) + + subject + end + end + end + + context 'with object storage enabled' do + context 'and direct upload enabled' do + let!(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + file: fog_file, + 'file.remote_id' => remote_id + } + end + + it 'responds with status 403' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'with valid remote_id' do + let(:params) do + { + file: fog_file, + 'file.remote_id' => file_name + } + end + + it 'creates package and stores package file' do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq(params[:file].original_filename) + expect(package_file.file.read).to eq('content') + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end +end + +RSpec.shared_examples 'workhorse authorization' do + it 'authorizes posting package with a valid token' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + it 'rejects request without a valid token' do + headers_with_token['HTTP_AUTHORIZATION'] = 'foo' + + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'rejects request without a valid permission' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'rejects requests that bypassed gitlab-workhorse' do + headers_with_token.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'when using remote storage' do + context 'when direct upload is enabled' do + before do + stub_package_file_object_storage(enabled: true, direct_upload: true) + end + + it 'responds with status 200, location of package remote store and object details' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response).not_to have_key('TempPath') + expect(json_response['RemoteObject']).to have_key('ID') + expect(json_response['RemoteObject']).to have_key('GetURL') + expect(json_response['RemoteObject']).to have_key('StoreURL') + expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['RemoteObject']).not_to have_key('MultipartUpload') + end + end + + context 'when direct upload is disabled' do + before do + stub_package_file_object_storage(enabled: true, direct_upload: false) + end + + it 'handles as a local file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(::Packages::PackageFileUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb index 8cbf11b6de1..f31cbcfdec1 100644 --- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| - let!(:custom_attribute1) { attributable.custom_attributes.create key: 'foo', value: 'foo' } - let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' } + let!(:custom_attribute1) { attributable.custom_attributes.create! key: 'foo', value: 'foo' } + let!(:custom_attribute2) { attributable.custom_attributes.create! key: 'bar', value: 'bar' } describe "GET /#{attributable_name} with custom attributes filter" do before do @@ -14,8 +14,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| get api("/#{attributable_name}", user), params: { custom_attributes: { foo: 'foo', bar: 'bar' } } expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to be 2 - expect(json_response.map { |r| r['id'] }).to contain_exactly attributable.id, other_attributable.id + expect(json_response.map { |r| r['id'] }).to include(attributable.id, other_attributable.id) end end @@ -40,7 +39,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| get api("/#{attributable_name}", user), params: { with_custom_attributes: true } expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to be 2 + expect(json_response).not_to be_empty expect(json_response.first).not_to include 'custom_attributes' end end @@ -50,16 +49,15 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| get api("/#{attributable_name}", admin) expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to be 2 + expect(json_response).not_to be_empty expect(json_response.first).not_to include 'custom_attributes' - expect(json_response.second).not_to include 'custom_attributes' end it 'includes custom attributes if requested' do get api("/#{attributable_name}", admin), params: { with_custom_attributes: true } expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to be 2 + expect(json_response).not_to be_empty attributable_response = json_response.find { |r| r['id'] == attributable.id } other_attributable_response = json_response.find { |r| r['id'] == other_attributable.id } @@ -132,7 +130,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| end context 'with an authorized user' do - it'returns a single custom attribute' do + it 'returns a single custom attribute' do get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb index 48824a4b0d2..62dbac3fd4d 100644 --- a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb @@ -8,3 +8,42 @@ RSpec.shared_examples 'when the snippet is not found' do it_behaves_like 'a mutation that returns top-level errors', errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end + +RSpec.shared_examples 'snippet edit usage data counters' do + context 'when user is sessionless' do + it 'does not track usage data actions' do + expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action) + + post_graphql_mutation(mutation, current_user: current_user) + end + end + + context 'when user is not sessionless' do + before do + session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') + session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] } + + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash)) + end + + cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id + end + + it 'tracks usage data actions', :clean_gitlab_redis_shared_state do + expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action) + + post_graphql_mutation(mutation) + end + + context 'when mutation result raises an error' do + it 'does not track usage data actions' do + mutation_vars[:title] = nil + + expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action) + + post_graphql_mutation(mutation) + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index fcdc594f258..6aac51a5903 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -175,7 +175,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with object storage enabled' do let(:tmp_object) do - fog_connection.directories.new(key: 'packages').files.create( + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang key: "tmp/uploads/#{file_name}", body: 'content' ) diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index 6f4a0236b66..c9a33701161 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -41,3 +41,88 @@ RSpec.shared_examples 'deploy token for package uploads' do end end end + +RSpec.shared_examples 'does not cause n^2 queries' do + it 'avoids N^2 database queries' do + # we create a package to set the baseline for expected queries from 1 package + create( + :npm_package, + name: "@#{project.root_namespace.path}/my-package", + project: project, + version: "0.0.1" + ) + + control = ActiveRecord::QueryRecorder.new do + get api(url) + end + + 5.times do |n| + create( + :npm_package, + name: "@#{project.root_namespace.path}/my-package", + project: project, + version: "#{n}.0.0" + ) + end + + expect do + get api(url) + end.not_to exceed_query_limit(control) + end +end + +RSpec.shared_examples 'job token for package GET requests' do + context 'with job token headers' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + project.add_developer(user) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + + context 'invalid user' do + let(:headers) { basic_auth_header('foo', job.token) } + + it_behaves_like 'returning response status', :unauthorized + end + end +end + +RSpec.shared_examples 'job token for package uploads' do + context 'with job token headers' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + project.add_developer(user) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) } + + it_behaves_like 'returning response status', :unauthorized + end + + context 'invalid user' do + let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) } + + it_behaves_like 'returning response status', :unauthorized + end + end +end diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 4954151b93b..715c494840e 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -58,7 +58,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member context 'with object storage enabled' do let(:tmp_object) do - fog_connection.directories.new(key: 'packages').files.create( + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang key: "tmp/uploads/#{file_name}", body: 'content' ) diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb index cfbb84dd099..051367fbe96 100644 --- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb @@ -77,3 +77,142 @@ RSpec.shared_examples 'raw snippet files' do end end end + +RSpec.shared_examples 'snippet file updates' do + let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } } + let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } } + let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } } + let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } } + let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } } + let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } } + let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } } + + context 'with various snippet file changes' do + using RSpec::Parameterized::TableSyntax + + where(:is_multi_file, :file_name, :content, :files, :status) do + true | nil | nil | [create_action] | :success + true | nil | nil | [update_action] | :success + true | nil | nil | [move_action] | :success + true | nil | nil | [delete_action] | :success + true | nil | nil | [create_action, update_action] | :success + true | 'foo.txt' | 'bar' | [create_action] | :bad_request + true | 'foo.txt' | 'bar' | nil | :bad_request + true | nil | nil | nil | :bad_request + true | 'foo.txt' | nil | [create_action] | :bad_request + true | nil | 'bar' | [create_action] | :bad_request + true | '' | nil | [create_action] | :bad_request + true | nil | '' | [create_action] | :bad_request + true | nil | nil | [bad_file_path] | :bad_request + true | nil | nil | [bad_previous_path] | :bad_request + true | nil | nil | [invalid_move] | :unprocessable_entity + + false | 'foo.txt' | 'bar' | nil | :success + false | 'foo.txt' | nil | nil | :success + false | nil | 'bar' | nil | :success + false | 'foo.txt' | 'bar' | [create_action] | :bad_request + false | nil | nil | nil | :bad_request + false | nil | '' | nil | :bad_request + false | nil | nil | [bad_file_path] | :bad_request + false | nil | nil | [bad_previous_path] | :bad_request + end + + with_them do + before do + allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file) + end + + it 'has the correct response' do + update_params = {}.tap do |params| + params[:files] = files if files + params[:file_name] = file_name if file_name + params[:content] = content if content + end + + update_snippet(params: update_params) + + expect(response).to have_gitlab_http_status(status) + end + end + + context 'when save fails due to a repository commit error' do + before do + allow_next_instance_of(Repository) do |instance| + allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError) + end + + update_snippet(params: { files: [create_action] }) + end + + it 'returns a bad request response' do + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end +end + +RSpec.shared_examples 'snippet non-file updates' do + it 'updates a snippet non-file attributes' do + new_description = 'New description' + new_title = 'New title' + new_visibility = 'internal' + + update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility }) + + snippet.reload + + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(snippet.description).to eq(new_description) + expect(snippet.visibility).to eq(new_visibility) + expect(snippet.title).to eq(new_title) + end + end +end + +RSpec.shared_examples 'snippet individual non-file updates' do + using RSpec::Parameterized::TableSyntax + + where(:attribute, :updated_value) do + :description | 'new description' + :title | 'new title' + :visibility | 'private' + end + + with_them do + it 'updates the attribute' do + params = { attribute => updated_value } + + expect { update_snippet(params: params) } + .to change { snippet.reload.send(attribute) }.to(updated_value) + end + end +end + +RSpec.shared_examples 'invalid snippet updates' do + it 'returns 404 for invalid snippet id' do + update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' }) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it 'returns 400 for missing parameters' do + update_snippet + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 400 if content is blank' do + update_snippet(params: { content: '' }) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 400 if title is blank' do + update_snippet(params: { title: '' }) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'title is empty' + end +end diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index a17163328f4..84ef7723b9b 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -2,6 +2,10 @@ RSpec.shared_examples 'update with repository actions' do context 'when the repository exists' do + before do + allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(false) + end + it 'commits the changes to the repository' do existing_blob = snippet.blobs.first new_file_name = existing_blob.path + '_new' |