# frozen_string_literal: true
require 'spec_helper'

RSpec.describe 'Git LFS API and storage', feature_category: :source_code_management do
  using RSpec::Parameterized::TableSyntax

  include LfsHttpHelpers
  include ProjectForksHelper
  include WorkhorseHelpers
  include WorkhorseLfsHelpers

  let_it_be(:project, reload: true) { create(:project, :empty_repo) }
  let_it_be(:user) { create(:user) }

  context 'with projects' do
    it_behaves_like 'LFS http requests' do
      let_it_be(:other_project, reload: true) { create(:project, :empty_repo) }

      let(:container) { project }
      let(:authorize_guest) { project.add_guest(user) }
      let(:authorize_download) { project.add_reporter(user) }
      let(:authorize_upload) { project.add_developer(user) }

      context 'project specific LFS settings' do
        let(:body) { upload_body(sample_object) }
        let(:response) { request && super() }

        before do
          authorize_upload
          project.update_attribute(:lfs_enabled, project_lfs_enabled)
        end

        describe 'LFS disabled in project' do
          let(:project_lfs_enabled) { false }

          context 'when uploading' do
            subject(:request) { post_lfs_json(batch_url(project), body, headers) }

            it_behaves_like 'LFS http 404 response'
          end

          context 'when downloading' do
            subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }

            it_behaves_like 'LFS http 404 response'
          end
        end

        describe 'LFS enabled in project' do
          let(:project_lfs_enabled) { true }

          context 'when uploading' do
            subject(:request) { post_lfs_json(batch_url(project), body, headers) }

            it_behaves_like 'LFS http 200 response'
          end

          context 'when downloading' do
            subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }

            context 'when LFS object is not linked to project' do
              it_behaves_like 'LFS http 404 response'
            end

            context 'when LFS object is linked to project' do
              before do
                project.lfs_objects << lfs_object
              end

              it_behaves_like 'LFS http 200 blob response'
            end

            context 'when a fork relationship' do
              let_it_be(:original_project) { create(:project, :public) }
              let(:forked_project) { fork_project(original_project, user, repository: true) }

              subject(:request) { get objects_url(forked_project, sample_oid), params: {}, headers: headers }

              before do
                forked_project.add_developer(user)
              end

              context 'when the LFS object is linked to the original project' do
                before do
                  create(:lfs_objects_project, project: original_project, lfs_object: lfs_object)
                end

                it_behaves_like 'LFS http 200 blob response'
              end

              context 'when the LFS object is not linked to the original project' do
                it_behaves_like 'LFS http 404 response'
              end
            end
          end
        end
      end

      describe 'when fetching LFS object' do
        subject(:request) { get objects_url(project, sample_oid), params: {}, headers: headers }

        let(:response) { request && super() }

        before do
          project.lfs_objects << lfs_object
        end

        context 'when LFS uses object storage' do
          before do
            authorize_download
          end

          context 'when proxy download is enabled' do
            before do
              stub_lfs_object_storage(proxy_download: true)
              lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
            end

            it 'responds with the workhorse send-url' do
              expect(response).to have_gitlab_http_status(:ok)
              expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
            end
          end

          context 'when proxy download is disabled' do
            before do
              stub_lfs_object_storage(proxy_download: false)
              lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
            end

            it 'responds with redirect' do
              expect(response).to have_gitlab_http_status(:found)
            end

            it 'responds with the file location' do
              expect(response.location).to include(lfs_object.reload.file.path)
            end
          end
        end

        context 'when deploy key is authorized' do
          let_it_be(:key) { create(:deploy_key) }

          let(:authorization) { authorize_deploy_key }

          before do
            project.deploy_keys << key
          end

          it_behaves_like 'LFS http 200 blob response'
        end

        context 'when using a user key (LFSToken)' do
          let(:authorization) { authorize_user_key }

          context 'when user allowed' do
            before do
              authorize_download
            end

            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) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when user is blocked' do
              let_it_be(:user) { create(:user, :blocked) }

              it_behaves_like 'LFS http 401 response'
            end
          end

          context 'when user not allowed' do
            it_behaves_like 'LFS http 404 response'
          end
        end

        context 'when build is authorized as' do
          let(:authorization) { authorize_ci_project }
          let(:pipeline) { create(:ci_empty_pipeline, project: project) }
          let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

          shared_examples 'can download LFS only from own projects' do
            context 'for owned project' do
              let_it_be(:project) { create(:project, namespace: user.namespace) }

              it_behaves_like 'LFS http 200 blob response'
            end

            context 'for member of project' do
              before do
                authorize_download
              end

              it_behaves_like 'LFS http 200 blob response'
            end

            context 'for other project' do
              let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

              it 'rejects downloading code' do
                expect(response).to have_gitlab_http_status(:not_found)
              end
            end
          end

          context 'administrator', :enable_admin_mode do
            let_it_be(:user) { create(:admin) }

            it_behaves_like 'can download LFS only from own projects'
          end

          context 'regular user' do
            it_behaves_like 'can download LFS only from own projects'
          end

          context 'does not have user' do
            let(:build) { create(:ci_build, :running, pipeline: pipeline) }

            it_behaves_like 'can download LFS only from own projects'
          end
        end
      end

      describe 'when handling LFS batch request' do
        subject(:request) { post_lfs_json batch_url(project), body, headers }

        let(:response) { request && super() }

        before do
          project.lfs_objects << lfs_object
        end

        shared_examples 'process authorization header' do |renew_authorization:|
          let(:response_authorization) do
            authorization_in_action(lfs_actions.first)
          end

          if renew_authorization
            context 'when the authorization comes from a user' do
              it 'returns a new valid LFS token authorization' do
                expect(response_authorization).not_to eq(authorization)
              end

              it 'returns a valid token' do
                username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)

                expect(username).to eq(user.username)
                expect(Gitlab::LfsToken.new(user, project).token_valid?(token)).to be_truthy
              end

              it 'generates only one new token per each request' do
                authorizations = lfs_actions.map do |action|
                  authorization_in_action(action)
                end.compact

                expect(authorizations.uniq.count).to eq 1
              end
            end
          else
            context 'when the authorization comes from a token' do
              it 'returns the same authorization header' do
                expect(response_authorization).to eq(authorization)
              end
            end
          end

          def lfs_actions
            json_response['objects'].map { |a| a['actions'] }.compact
          end

          def authorization_in_action(action)
            (action['upload'] || action['download']).dig('header', 'Authorization')
          end
        end

        describe 'download' do
          let(:body) { download_body(sample_object) }

          shared_examples 'an authorized request' do |renew_authorization:|
            context 'when downloading an LFS object that is assigned to our project' do
              it_behaves_like 'LFS http 200 response'

              it 'with href to download' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
              end

              it_behaves_like 'process authorization header', renew_authorization: renew_authorization
            end

            context 'when downloading an LFS object that is assigned to other project' do
              before do
                lfs_object.update!(projects: [other_project])
              end

              it_behaves_like 'LFS http 200 response'

              it 'with an 404 for specific object' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
              end
            end

            context 'when downloading a LFS object that does not exist' do
              let(:body) { download_body(non_existing_object) }

              it_behaves_like 'LFS http 200 response'

              it 'with an 404 for specific object' do
                expect(json_response['objects'].first).to include(non_existing_object)
                expect(json_response['objects'].first['error']).to include('code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it")
              end
            end

            context 'when downloading one existing and one missing LFS object' do
              let(:body) { download_body(multiple_objects) }

              it_behaves_like 'LFS http 200 response'

              it 'responds with download hypermedia link for the existing object' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))
                expect(json_response['objects'].last).to eq({
                  'oid' => non_existing_object_oid,
                  'size' => non_existing_object_size,
                  'error' => {
                    'code' => 404,
                    'message' => "Object does not exist on the server or you don't have permissions to access it"
                  }
                })
              end

              it_behaves_like 'process authorization header', renew_authorization: renew_authorization
            end

            context 'when downloading two existing LFS objects' do
              let(:body) { download_body(multiple_objects) }
              let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) }

              before do
                project.lfs_objects << other_object
              end

              it 'responds with the download hypermedia link for each object' do
                expect(json_response['objects'].first).to include(sample_object)
                expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid))

                expect(json_response['objects'].last).to include(non_existing_object)
                expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid))
              end

              it_behaves_like 'process authorization header', renew_authorization: renew_authorization
            end

            context 'when downloading an LFS object that is stored on object storage' do
              before do
                stub_lfs_object_storage
                lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
              end

              context 'when lfs.object_store.proxy_download=true' do
                before do
                  stub_lfs_object_storage(proxy_download: true)
                end

                it_behaves_like 'LFS http 200 response'

                it 'does return proxied address URL' do
                  expect(json_response['objects'].first).to include(sample_object)
                  expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
                end
              end

              context 'when "lfs.object_store.proxy_download" is "false"' do
                before do
                  stub_lfs_object_storage(proxy_download: false)
                end

                it_behaves_like 'LFS http 200 response'

                it 'does return direct object storage URL' do
                  expect(json_response['objects'].first).to include(sample_object)
                  expect(json_response['objects'].first['actions']['download']['href']).to start_with("https://lfs-objects.s3.amazonaws.com/")
                  expect(json_response['objects'].first['actions']['download']['href']).to include("X-Amz-Expires=3600&")
                end
              end
            end

            context 'when sending objects=[]' do
              let(:body) { download_body([]) }

              it_behaves_like 'LFS http expected response code and message' do
                let(:response_code) { 404 }
                let(:message) { 'Not found.' }
              end
            end
          end

          context 'when user is authenticated' do
            before do
              project.add_role(user, role) if role
            end

            it_behaves_like 'an authorized request', renew_authorization: true do
              let(:role) { :reporter }
            end

            context 'when user is not a member of the project' do
              let(:role) { nil }

              it_behaves_like 'LFS http 404 response'
            end

            context 'when user does not have download access' do
              let(:role) { :guest }

              it_behaves_like 'LFS http 404 response'
            end

            context 'when user password is expired' do
              let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) }

              let(:role) { :reporter }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when user is blocked' do
              let_it_be(:user) { create(:user, :blocked) }

              let(:role) { :reporter }

              it_behaves_like 'LFS http 401 response'
            end
          end

          context 'when using Deploy Tokens' do
            let(:authorization) { authorize_deploy_token }

            context 'when Deploy Token is not valid' do
              let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when Deploy Token is not related to the project' do
              let(:deploy_token) { create(:deploy_token, projects: [other_project]) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when deploy token is from an unrelated group to the project' do
              let(:group) { create(:group) }
              let(:deploy_token) { create(:deploy_token, :group, groups: [group]) }

              it_behaves_like 'LFS http 401 response'
            end

            context 'when deploy token is from a parent group of the project and valid' do
              let(:group) { create(:group) }
              let(:project) { create(:project, group: group) }
              let(:deploy_token) { create(:deploy_token, :group, groups: [group]) }

              it_behaves_like 'an authorized request', renew_authorization: false
            end

            # TODO: We should fix this test case that causes flakyness by alternating the result of the above test cases.
            context 'when Deploy Token is valid' do
              let(:deploy_token) { create(:deploy_token, projects: [project]) }

              it_behaves_like 'an authorized request', renew_authorization: false
            end
          end

          context 'when build is authorized as' do
            let(:authorization) { authorize_ci_project }

            shared_examples 'can download LFS only from own projects' do |renew_authorization:|
              context 'for own project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: project) }

                before do
                  authorize_download
                end

                it_behaves_like 'an authorized request', renew_authorization: renew_authorization
              end

              context 'for other project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                it 'rejects downloading code' do
                  expect(response).to have_gitlab_http_status(:not_found)
                end
              end
            end

            context 'administrator', :enable_admin_mode do
              let_it_be(:user) { create(:admin) }

              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              it_behaves_like 'can download LFS only from own projects', renew_authorization: true
            end

            context 'regular user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              it_behaves_like 'can download LFS only from own projects', renew_authorization: true
            end

            context 'does not have user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline) }

              it_behaves_like 'can download LFS only from own projects', renew_authorization: false
            end
          end

          context 'when user is not authenticated' do
            let(:authorization) { nil }

            describe 'is accessing public project' do
              let_it_be(:project) { create(:project, :public) }

              it_behaves_like 'LFS http 200 response'

              it 'returns href to download' do
                expect(json_response).to eq({
                  'objects' => [
                    {
                      'oid' => sample_oid,
                      'size' => sample_size,
                      'authenticated' => true,
                      'actions' => {
                        'download' => {
                          'href' => objects_url(project, sample_oid),
                          'header' => {}
                        }
                      }
                    }
                  ]
                })
              end
            end

            describe 'is accessing non-public project' do
              it_behaves_like 'LFS http 401 response'
            end
          end
        end

        describe 'upload' do
          let_it_be(:project) { create(:project, :public) }

          let(:body) { upload_body(sample_object) }

          shared_examples 'pushes new LFS objects' do |renew_authorization:|
            let(:sample_size) { 150.megabytes }
            let(:sample_oid) { non_existing_object_oid }

            it_behaves_like 'LFS http 200 response'

            it 'responds with upload hypermedia link' do
              expect(json_response['objects']).to be_kind_of(Array)
              expect(json_response['objects'].first).to include(sample_object)
              expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))

              headers = json_response['objects'].first['actions']['upload']['header']
              expect(headers['Content-Type']).to eq('application/octet-stream')
              expect(headers['Transfer-Encoding']).to eq('chunked')
            end

            it_behaves_like 'process authorization header', renew_authorization: renew_authorization
          end

          describe 'when request is authenticated' do
            describe 'when user has project push access' do
              before do
                authorize_upload
              end

              context 'when pushing an LFS object that already exists' do
                shared_examples_for 'batch upload with existing LFS object' do
                  it_behaves_like 'LFS http 200 response'

                  it 'responds with links to the object in the project' do
                    expect(json_response['objects']).to be_kind_of(Array)
                    expect(json_response['objects'].first).to include(sample_object)
                    expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
                    expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
                    expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size))

                    headers = json_response['objects'].first['actions']['upload']['header']
                    expect(headers['Content-Type']).to eq('application/octet-stream')
                    expect(headers['Transfer-Encoding']).to eq('chunked')
                  end

                  it_behaves_like 'process authorization header', renew_authorization: true
                end

                context 'in another project' do
                  before do
                    lfs_object.update!(projects: [other_project])
                  end

                  it_behaves_like 'batch upload with existing LFS object'
                end

                context 'in source of fork project' do
                  let(:other_project) { create(:project, :empty_repo) }
                  let(:project) { fork_project(other_project) }

                  before do
                    lfs_object.update!(projects: [other_project])
                  end

                  context 'when user has access to both the parent and fork' do
                    before do
                      project.add_developer(user)
                      other_project.add_developer(user)
                    end

                    it 'links existing LFS objects to other project' do
                      expect(Gitlab::AppJsonLogger).to receive(:info).with(
                        message: "LFS object auto-linked to forked project",
                        lfs_object_oid: lfs_object.oid,
                        lfs_object_size: lfs_object.size,
                        source_project_id: other_project.id,
                        source_project_path: other_project.full_path,
                        target_project_id: project.id,
                        target_project_path: project.full_path).and_call_original
                      expect(json_response['objects']).to be_kind_of(Array)
                      expect(json_response['objects'].first).to include(sample_object)
                      expect(json_response['objects'].first).not_to have_key('actions')

                      expect(lfs_object.reload.projects.pluck(:id)).to match_array([other_project.id, project.id])
                    end
                  end

                  context 'when user does not have access to parent' do
                    before do
                      project.add_developer(user)
                    end

                    it_behaves_like 'batch upload with existing LFS object'
                  end
                end
              end

              context 'when pushing a LFS object that does not exist' do
                it_behaves_like 'pushes new LFS objects', renew_authorization: true
              end

              context 'when pushing one new and one existing LFS object' do
                let(:body) { upload_body(multiple_objects) }

                it_behaves_like 'LFS http 200 response'

                it 'responds with upload hypermedia link for the new object' do
                  expect(json_response['objects']).to be_kind_of(Array)

                  expect(json_response['objects'].first).to include(sample_object)
                  expect(json_response['objects'].first).not_to have_key('actions')

                  expect(json_response['objects'].last).to include(non_existing_object)
                  expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size))

                  headers = json_response['objects'].last['actions']['upload']['header']
                  expect(headers['Content-Type']).to eq('application/octet-stream')
                  expect(headers['Transfer-Encoding']).to eq('chunked')
                end

                it_behaves_like 'process authorization header', renew_authorization: true
              end
            end

            context 'when user does not have push access' do
              it_behaves_like 'LFS http 403 response'
            end

            context 'when build is authorized' do
              let(:authorization) { authorize_ci_project }
              let(:pipeline) { create(:ci_empty_pipeline, project: project) }

              context 'build has an user' do
                let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

                context 'tries to push to own project' do
                  it_behaves_like 'LFS http 403 response'
                end

                context 'tries to push to other project' do
                  let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                  it_behaves_like 'LFS http 404 response'
                end
              end

              context 'does not have user' do
                let(:build) { create(:ci_build, :running, pipeline: pipeline) }

                it_behaves_like 'LFS http 403 response'
              end
            end

            context 'when deploy key has project push access' do
              let(:key) { create(:deploy_key) }
              let(:authorization) { authorize_deploy_key }

              before do
                project.deploy_keys_projects.create!(deploy_key: key, can_push: true)
              end

              it_behaves_like 'pushes new LFS objects', renew_authorization: false
            end
          end

          context 'when user is not authenticated' do
            let(:authorization) { nil }

            context 'when user has push access' do
              before do
                authorize_upload
              end

              it_behaves_like 'LFS http 401 response'
            end

            context 'when user does not have push access' do
              it_behaves_like 'LFS http 401 response'
            end
          end
        end

        describe 'unsupported' do
          let(:body) { request_body('other', sample_object) }

          it_behaves_like 'LFS http 404 response'
        end
      end

      describe 'when handling LFS batch request on a read-only GitLab instance' do
        subject { post_lfs_json(batch_url(project), body, headers) }

        before do
          allow(Gitlab::Database).to receive(:read_only?) { true }

          project.add_maintainer(user)

          subject
        end

        context 'when downloading' do
          let(:body) { download_body(sample_object) }

          it_behaves_like 'LFS http 200 response'
        end

        context 'when uploading' do
          let(:body) { upload_body(sample_object) }

          it_behaves_like 'LFS http expected response code and message' do
            let(:response_code) { 403 }
            let(:message) { 'You cannot write to this read-only GitLab instance.' }
          end
        end
      end

      describe 'when pushing a LFS object' do
        let(:include_workhorse_jwt_header) { true }

        shared_examples 'unauthorized' do
          context 'and request is sent by gitlab-workhorse to authorize the request' do
            before do
              put_authorize
            end

            it_behaves_like 'LFS http 401 response'
          end

          context 'and request is sent by gitlab-workhorse to finalize the upload' do
            before do
              put_finalize
            end

            it_behaves_like 'LFS http 401 response'
          end

          context 'and request is sent with a malformed headers' do
            before do
              put_finalize('/etc/passwd')
            end

            it_behaves_like 'LFS http 401 response'
          end
        end

        shared_examples 'forbidden' do
          context 'and request is sent by gitlab-workhorse to authorize the request' do
            before do
              put_authorize
            end

            it_behaves_like 'LFS http 403 response'
          end

          context 'and request is sent by gitlab-workhorse to finalize the upload' do
            before do
              put_finalize
            end

            it_behaves_like 'LFS http 403 response'
          end

          context 'and request is sent with a malformed headers' do
            before do
              put_finalize('/etc/passwd')
            end

            it_behaves_like 'LFS http 403 response'
          end
        end

        describe 'to one project' do
          describe 'when user is authenticated' do
            describe 'when user has push access to the project' do
              before do
                project.add_developer(user)
              end

              context 'and the request bypassed workhorse' do
                it 'raises an exception' do
                  expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
                end
              end

              context 'and request is sent by gitlab-workhorse to authorize the request' do
                shared_examples 'a valid response' do
                  before do
                    put_authorize
                  end

                  it_behaves_like 'LFS http 200 workhorse response'
                end

                shared_examples 'a local file' do
                  it_behaves_like 'a valid response' do
                    it 'responds with status 200, location of LFS store and object details' do
                      expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
                      expect(json_response['RemoteObject']).to be_nil
                      expect(json_response['LfsOid']).to eq(sample_oid)
                      expect(json_response['LfsSize']).to eq(sample_size)
                    end
                  end
                end

                context 'when using local storage' do
                  it_behaves_like 'a local file'
                end

                context 'when using remote storage' do
                  context 'when direct upload is enabled' do
                    before do
                      stub_lfs_object_storage(enabled: true, direct_upload: true)
                    end

                    it_behaves_like 'a valid response' do
                      it 'responds with status 200, location of LFS remote store and object details' do
                        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']).to have_key('MultipartUpload')
                        expect(json_response['LfsOid']).to eq(sample_oid)
                        expect(json_response['LfsSize']).to eq(sample_size)
                      end
                    end
                  end

                  context 'when direct upload is disabled' do
                    before do
                      stub_lfs_object_storage(enabled: true, direct_upload: false)
                    end

                    it_behaves_like 'a local file'
                  end
                end
              end

              context 'and request is sent by gitlab-workhorse to finalize the upload' do
                before do
                  put_finalize
                end

                it_behaves_like 'LFS http 200 response'

                it 'LFS object is linked to the project' do
                  expect(lfs_object.projects.pluck(:id)).to include(project.id)
                end
              end

              context 'and request to finalize the upload is not sent by gitlab-workhorse' do
                it 'fails with a JWT decode error' do
                  expect { put_finalize(verified: false) }.to raise_error(JWT::DecodeError)
                end
              end

              context 'and the uploaded file is invalid' do
                where(:size, :sha256, :status) do
                  nil | nil | :ok # Test setup sanity check
                  0 | nil | :bad_request
                  nil | ('a' * 64) | :bad_request
                end

                with_them do
                  it 'validates the upload size and SHA256' do
                    put_finalize(size: size, sha256: sha256)

                    expect(response).to have_gitlab_http_status(status)
                  end
                end
              end

              context 'and workhorse requests upload finalize for a new LFS object' do
                before do
                  lfs_object.destroy!
                end

                context 'with object storage enabled' do
                  context 'and direct upload enabled' do
                    let!(:fog_connection) do
                      stub_lfs_object_storage(direct_upload: true)
                    end

                    let(:tmp_object) do
                      fog_connection.directories.new(key: 'lfs-objects').files.create( # rubocop: disable Rails/SaveBang
                        key: 'tmp/uploads/12312300',
                        body: 'x' * sample_size
                      )
                    end

                    ['123123', '../../123123'].each do |remote_id|
                      context "with invalid remote_id: #{remote_id}" do
                        subject do
                          put_finalize(remote_object: tmp_object, args: {
                            '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
                      subject do
                        put_finalize(remote_object: tmp_object, args: {
                          'file.remote_id' => '12312300',
                          'file.name' => 'name'
                        })
                      end

                      it 'responds with status 200' do
                        subject

                        expect(response).to have_gitlab_http_status(:ok)

                        object = LfsObject.find_by_oid(sample_oid)
                        expect(object).to be_present
                        expect(object.file.read).to eq(tmp_object.body)
                      end

                      it 'schedules migration of file to object storage' do
                        subject

                        expect(LfsObject.last.projects).to include(project)
                      end

                      it 'have valid file' do
                        subject

                        expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
                        expect(LfsObject.last.file).to be_exists
                      end
                    end
                  end
                end
              end

              context 'without the lfs object' do
                before do
                  lfs_object.destroy!
                end

                it 'rejects slashes in the tempfile name (path traversal)' do
                  put_finalize('../bar', with_tempfile: true)
                  expect(response).to have_gitlab_http_status(:bad_request)
                end

                context 'not sending the workhorse jwt header' do
                  let(:include_workhorse_jwt_header) { false }

                  it 'rejects the request' do
                    put_finalize(with_tempfile: true)

                    expect(response).to have_gitlab_http_status(:unprocessable_entity)
                  end
                end
              end
            end

            describe 'and user does not have push access' do
              before do
                project.add_reporter(user)
              end

              it_behaves_like 'forbidden'
            end
          end

          context 'when build is authorized' do
            let(:authorization) { authorize_ci_project }
            let(:pipeline) { create(:ci_empty_pipeline, project: project) }

            context 'build has an user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              context 'tries to push to own project' do
                before do
                  project.ci_push_repository_for_job_token_allowed = push_repository_for_job_token_allowed
                  project.save!
                  project.add_developer(user)
                  put_authorize
                end

                context 'when project is not allowed to push' do
                  let(:push_repository_for_job_token_allowed) { false }

                  it_behaves_like 'LFS http 403 response'
                end

                context 'when project is allowed to push' do
                  let(:push_repository_for_job_token_allowed) { true }

                  it_behaves_like 'LFS http 200 workhorse response'
                end
              end

              context 'tries to push to other project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                before do
                  put_authorize
                end

                it_behaves_like 'LFS http 404 response'
              end
            end

            context 'does not have user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline) }

              before do
                put_authorize
              end

              it_behaves_like 'LFS http 403 response'
            end
          end

          describe 'when using a user key (LFSToken)' do
            let(:authorization) { authorize_user_key }

            context 'when user allowed' do
              before do
                project.add_developer(user)
                put_authorize
              end

              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) }

                it_behaves_like 'LFS http 401 response'
              end

              context 'when user is blocked' do
                let_it_be(:user) { create(:user, :blocked) }

                it_behaves_like 'LFS http 401 response'
              end
            end

            context 'when user not allowed' do
              before do
                put_authorize
              end

              it_behaves_like 'LFS http 404 response'
            end
          end

          context 'for unauthenticated' do
            let(:authorization) { nil }

            it_behaves_like 'unauthorized'
          end
        end

        describe 'to a forked project' do
          let_it_be_with_reload(:upstream_project) { create(:project, :public) }
          let_it_be(:project_owner) { create(:user) }

          let(:project) { fork_project(upstream_project, project_owner) }

          describe 'when user is authenticated' do
            describe 'when user has push access to the project' do
              before do
                project.add_developer(user)
              end

              context 'and request is sent by gitlab-workhorse to authorize the request' do
                before do
                  put_authorize
                end

                it_behaves_like 'LFS http 200 workhorse response'

                it 'with location of LFS store and object details' do
                  expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
                  expect(json_response['LfsOid']).to eq(sample_oid)
                  expect(json_response['LfsSize']).to eq(sample_size)
                end
              end

              context 'and request is sent by gitlab-workhorse to finalize the upload' do
                before do
                  put_finalize
                end

                it_behaves_like 'LFS http 200 response'

                it 'LFS object is linked to the forked project' do
                  expect(lfs_object.projects.pluck(:id)).to include(project.id)
                end
              end
            end

            describe 'when user has push access to upstream project' do
              before do
                upstream_project.add_maintainer(user)
              end

              context 'an MR exists on target forked project' do
                let(:allow_collaboration) { true }
                let(:merge_request) do
                  create(
                    :merge_request,
                    target_project: upstream_project,
                    source_project: project,
                    allow_collaboration: allow_collaboration
                  )
                end

                before do
                  merge_request
                end

                context 'with allow_collaboration option set to true' do
                  context 'and request is sent by gitlab-workhorse to authorize the request' do
                    before do
                      put_authorize
                    end

                    it_behaves_like 'LFS http 200 workhorse response'
                  end

                  context 'and request is sent by gitlab-workhorse to finalize the upload' do
                    before do
                      put_finalize
                    end

                    it_behaves_like 'LFS http 200 response'
                  end
                end

                context 'with allow_collaboration option set to false' do
                  context 'request is sent by gitlab-workhorse to authorize the request' do
                    let(:allow_collaboration) { false }

                    before do
                      put_authorize
                    end

                    it_behaves_like 'forbidden'
                  end
                end
              end
            end

            describe 'and user does not have push access' do
              it_behaves_like 'forbidden'
            end
          end

          context 'when build is authorized' do
            let(:authorization) { authorize_ci_project }
            let(:pipeline) { create(:ci_empty_pipeline, project: project) }

            before do
              put_authorize
            end

            context 'build has an user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }

              context 'tries to push to own project' do
                it_behaves_like 'LFS http 403 response'
              end

              context 'tries to push to other project' do
                let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }

                it_behaves_like 'LFS http 404 response'
              end
            end

            context 'does not have user' do
              let(:build) { create(:ci_build, :running, pipeline: pipeline) }

              it_behaves_like 'LFS http 403 response'
            end
          end

          context 'for unauthenticated' do
            let(:authorization) { nil }

            it_behaves_like 'unauthorized'
          end

          describe 'and second project not related to fork or a source project' do
            let_it_be(:second_project) { create(:project) }

            before do
              second_project.add_maintainer(user)
              upstream_project.lfs_objects << lfs_object
            end

            context 'when pushing the same LFS object to the second project' do
              before do
                put_finalize(with_tempfile: true, to_project: second_project)
              end

              it_behaves_like 'LFS http 200 response'

              it 'links the LFS object to the project' do
                expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
              end
            end
          end
        end

        def put_authorize(verified: true)
          authorize_headers = headers
          authorize_headers.merge!(workhorse_internal_api_request_header) if verified

          put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
        end
      end
    end
  end

  context 'with project wikis' do
    it_behaves_like 'LFS http requests' do
      let(:container) { create(:project_wiki, :empty_repo, project: project) }
      let(:authorize_guest) { project.add_guest(user) }
      let(:authorize_download) { project.add_reporter(user) }
      let(:authorize_upload) { project.add_developer(user) }
    end
  end

  context 'with snippets' do
    # LFS is not supported on snippets, so we override the shared examples
    # to expect 404 responses instead.
    [
      'LFS http 200 response',
      'LFS http 200 blob response',
      'LFS http 403 response'
    ].each do |examples|
      shared_examples_for(examples) { it_behaves_like 'LFS http 404 response' }
    end

    context 'with project snippets' do
      it_behaves_like 'LFS http requests' do
        let(:container) { create(:project_snippet, :empty_repo, project: project) }
        let(:authorize_guest) { project.add_guest(user) }
        let(:authorize_download) { project.add_reporter(user) }
        let(:authorize_upload) { project.add_developer(user) }
      end
    end

    context 'with personal snippets' do
      it_behaves_like 'LFS http requests' do
        let(:container) { create(:personal_snippet, :empty_repo) }
        let(:authorize_upload) { container.update!(author: user) }
      end
    end
  end
end
