# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Lfs::Client do
  let(:base_url) { "https://example.com" }
  let(:username) { 'user' }
  let(:password) { 'password' }
  let(:credentials) { { user: username, password: password } }
  let(:git_lfs_content_type) { 'application/vnd.git-lfs+json' }
  let(:git_lfs_user_agent) { "GitLab #{Gitlab::VERSION} LFS client" }

  let(:basic_auth_headers) do
    { 'Authorization' => "Basic #{Base64.strict_encode64("#{username}:#{password}")}" }
  end

  let(:upload_action) do
    {
      "href" => "#{base_url}/some/file",
      "header" => {
        "Key" => "value"
      }
    }
  end

  let(:verify_action) do
    {
      "href" => "#{base_url}/some/file/verify",
      "header" => {
        "Key" => "value"
      }
    }
  end

  let(:authorized_upload_action) { upload_action.tap { |action| action['header']['Authorization'] = 'foo' } }
  let(:authorized_verify_action) { verify_action.tap { |action| action['header']['Authorization'] = 'foo' } }

  subject(:lfs_client) { described_class.new(base_url, credentials: credentials) }

  describe '#batch' do
    let_it_be(:objects) { create_list(:lfs_object, 3) }

    context 'server returns 200 OK' do
      it 'makes a successful batch request' do
        stub = stub_batch(
          objects: objects,
          headers: basic_auth_headers
        ).to_return(
          status: 200,
          body: { 'objects' => 'anything', 'transfer' => 'basic' }.to_json,
          headers: { 'Content-Type' => git_lfs_content_type }
        )

        result = lfs_client.batch!('upload', objects)

        expect(stub).to have_been_requested
        expect(result).to eq('objects' => 'anything', 'transfer' => 'basic')
      end
    end

    context 'server returns 400 error' do
      it 'raises an error' do
        stub_batch(objects: objects, headers: basic_auth_headers).to_return(status: 400)

        expect { lfs_client.batch!('upload', objects) }.to raise_error(/Failed/)
      end
    end

    context 'server returns 500 error' do
      it 'raises an error' do
        stub_batch(objects: objects, headers: basic_auth_headers).to_return(status: 400)

        expect { lfs_client.batch!('upload', objects) }.to raise_error(/Failed/)
      end
    end

    context 'server returns an exotic transfer method' do
      it 'raises an error' do
        stub_batch(
          objects: objects,
          headers: basic_auth_headers
        ).to_return(
          status: 200,
          body: { 'transfer' => 'carrier-pigeon' }.to_json,
          headers: { 'Content-Type' => git_lfs_content_type }
        )

        expect { lfs_client.batch!('upload', objects) }.to raise_error(/Unsupported transfer/)
      end
    end

    def stub_batch(objects:, headers:, operation: 'upload', transfer: 'basic')
      objects = objects.as_json(only: [:oid, :size])
      body = { operation: operation, transfers: [transfer], objects: objects }.to_json

      headers = {
        'Accept' => git_lfs_content_type,
        'Content-Type' => git_lfs_content_type,
        'User-Agent' => git_lfs_user_agent
      }.merge(headers)

      stub_request(:post, base_url + '/info/lfs/objects/batch').with(body: body, headers: headers)
    end
  end

  describe "#upload" do
    let_it_be(:object) { create(:lfs_object) }

    context 'server returns 200 OK to an authenticated request' do
      it "makes an HTTP PUT with expected parameters" do
        stub_upload(object: object, headers: upload_action['header']).to_return(status: 200)

        lfs_client.upload!(object, upload_action, authenticated: true)
      end
    end

    context 'server returns 200 OK with a chunked transfer request' do
      before do
        upload_action['header']['Transfer-Encoding'] = 'gzip, chunked'
      end

      it "makes an HTTP PUT with expected parameters" do
        stub_upload(object: object, headers: upload_action['header'], chunked_transfer: true).to_return(status: 200)

        lfs_client.upload!(object, upload_action, authenticated: true)
      end
    end

    context 'server returns 200 OK with a username and password in the URL' do
      let(:base_url) { "https://someuser:testpass@example.com" }

      it "makes an HTTP PUT with expected parameters" do
        stub_upload(
          object: object,
          headers: basic_auth_headers.merge(upload_action['header']),
          url: "https://example.com/some/file"
        ).to_return(status: 200)

        lfs_client.upload!(object, upload_action, authenticated: true)
      end
    end

    context 'no credentials in client' do
      subject(:lfs_client) { described_class.new(base_url, credentials: {}) }

      context 'server returns 200 OK with credentials in URL' do
        let(:creds) { 'someuser:testpass' }
        let(:base_url) { "https://#{creds}@example.com" }
        let(:auth_headers) { { 'Authorization' => "Basic #{Base64.strict_encode64(creds)}" } }

        it "makes an HTTP PUT with expected parameters" do
          stub_upload(
            object: object,
            headers: auth_headers.merge(upload_action['header']),
            url: "https://example.com/some/file"
          ).to_return(status: 200)

          lfs_client.upload!(object, upload_action, authenticated: true)
        end
      end
    end

    context 'server returns 200 OK to an unauthenticated request' do
      it "makes an HTTP PUT with expected parameters" do
        stub = stub_upload(
          object: object,
          headers: basic_auth_headers.merge(upload_action['header'])
        ).to_return(status: 200)

        lfs_client.upload!(object, upload_action, authenticated: false)

        expect(stub).to have_been_requested
      end
    end

    context 'request is not marked as authenticated but includes an authorization header' do
      it 'prefers the provided authorization header' do
        stub = stub_upload(
          object: object,
          headers: authorized_upload_action['header']
        ).to_return(status: 200)

        lfs_client.upload!(object, authorized_upload_action, authenticated: false)

        expect(stub).to have_been_requested
      end
    end

    context 'LFS object has no file' do
      let(:object) { LfsObject.new }

      it 'makes an HTTP PUT with expected parameters' do
        stub = stub_upload(
          object: object,
          headers: upload_action['header']
        ).to_return(status: 200)

        lfs_client.upload!(object, upload_action, authenticated: true)

        expect(stub).to have_been_requested
      end
    end

    context 'server returns 400 error' do
      it 'raises an error' do
        stub_upload(object: object, headers: upload_action['header']).to_return(status: 400)

        expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed to upload object: HTTP status 400/)
      end
    end

    context 'server returns 500 error' do
      it 'raises an error' do
        stub_upload(object: object, headers: upload_action['header']).to_return(status: 500)

        expect { lfs_client.upload!(object, upload_action, authenticated: true) }.to raise_error(/Failed to upload object: HTTP status 500/)
      end
    end

    def stub_upload(object:, headers:, url: upload_action['href'], chunked_transfer: false)
      headers = {
        'Content-Type' => 'application/octet-stream',
        'User-Agent' => git_lfs_user_agent
      }.merge(headers)

      if chunked_transfer
        headers['Transfer-Encoding'] = 'gzip, chunked'
      else
        headers['Content-Length'] = object.size.to_s
      end

      stub_request(:put, url).with(
        body: object.file.read,
        headers: headers
      )
    end
  end

  describe "#verify" do
    let_it_be(:object) { create(:lfs_object) }

    context 'server returns 200 OK to an authenticated request' do
      it "makes an HTTP POST with expected parameters" do
        stub_verify(object: object, headers: verify_action['header']).to_return(status: 200)

        lfs_client.verify!(object, verify_action, authenticated: true)
      end
    end

    context 'server returns 200 OK with a username and password in the URL' do
      let(:base_url) { "https://someuser:testpass@example.com" }

      it "makes an HTTP PUT with expected parameters" do
        stub_verify(
          object: object,
          headers: basic_auth_headers.merge(verify_action['header']),
          url: "https://example.com/some/file/verify"
        ).to_return(status: 200)

        lfs_client.verify!(object, verify_action, authenticated: true)
      end
    end

    context 'server returns 200 OK to an unauthenticated request' do
      it "makes an HTTP POST with expected parameters" do
        stub = stub_verify(
          object: object,
          headers: basic_auth_headers.merge(verify_action['header'])
        ).to_return(status: 200)

        lfs_client.verify!(object, verify_action, authenticated: false)

        expect(stub).to have_been_requested
      end
    end

    context 'request is not marked as authenticated but includes an authorization header' do
      it 'prefers the provided authorization header' do
        stub = stub_verify(
          object: object,
          headers: authorized_verify_action['header']
        ).to_return(status: 200)

        lfs_client.verify!(object, authorized_verify_action, authenticated: false)

        expect(stub).to have_been_requested
      end
    end

    context 'server returns 400 error' do
      it 'raises an error' do
        stub_verify(object: object, headers: verify_action['header']).to_return(status: 400)

        expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed to verify object: HTTP status 400/)
      end
    end

    context 'server returns 500 error' do
      it 'raises an error' do
        stub_verify(object: object, headers: verify_action['header']).to_return(status: 500)

        expect { lfs_client.verify!(object, verify_action, authenticated: true) }.to raise_error(/Failed to verify object: HTTP status 500/)
      end
    end

    def stub_verify(object:, headers:, url: verify_action['href'])
      headers = {
        'Accept' => git_lfs_content_type,
        'Content-Type' => git_lfs_content_type,
        'User-Agent' => git_lfs_user_agent
      }.merge(headers)

      stub_request(:post, url).with(
        body: object.to_json(only: [:oid, :size]),
        headers: headers
      )
    end
  end
end
