# frozen_string_literal: true

require_relative 'spec_helper'
require 'base64'

module Aws
  module S3
    describe PresignedPost do
      let(:creds) { Credentials.new('akid', 'secret') }

      let(:region) { 'us-east-1' }

      let(:bucket) { 'bucket-name' }

      let(:options) { {} }

      let(:post) { PresignedPost.new(creds, region, bucket, options) }

      def decode(policy)
        Json.load(Base64.decode64(policy))
      end

      def policy(post)
        decode(post.fields['policy'])['conditions']
      end

      describe '#initialize' do
        it 'rejects unknown options' do
          expect do
            PresignedPost.new(creds, region, bucket, foo: 'bar')
          end.to raise_error(/Unsupported option: foo/)
        end

        it 'provides options for setting fields or fields starts with' do
          post = PresignedPost.new(creds, region, bucket, key: 'key',
                                   acl: 'public-read',
                                   content_type_starts_with: 'image/')
          expect(post.fields['acl']).to eq('public-read')
          expect(policy(post)).to include(['starts-with', '$Content-Type', 'image/'])
        end
      end

      describe '#url' do
        it 'puts dns compatible bucket names in the host' do
          post = PresignedPost.new(creds, 'us-east-1', 'bucket-name')
          expect(post.url).to eq('https://bucket-name.s3.amazonaws.com')
        end

        it 'puts dns compatible bucket name that contain dots in the path' do
          post = PresignedPost.new(creds, 'us-east-1', 'bucket.name')
          expect(post.url).to eq('https://s3.amazonaws.com/bucket.name')
        end

        it 'puts non-dns compatible bucket name in the path' do
          post = PresignedPost.new(creds, 'us-east-1', 'BucketName')
          expect(post.url).to eq('https://s3.amazonaws.com/BucketName')
        end

        it 'creates regionalized urls for other regions' do
          post = PresignedPost.new(creds, 'eu-central-1', 'bucket-name')
          expect(post.url).to eq(
            'https://bucket-name.s3.eu-central-1.amazonaws.com'
          )
        end

        it 'can use an accelerated endpoint' do
          post = PresignedPost.new(
            creds, 'us-east-1', 'bucket-name', use_accelerate_endpoint: true
          )
          expect(post.url).to eq(
            'https://bucket-name.s3-accelerate.amazonaws.com'
          )
        end
      end

      describe 'key' do
        it 'is required' do
          expect { post.fields }.to raise_error(/you must provide a key/)
        end

        it 'can be set via :key' do
          options[:key] = 'obj-key'
          expect(post.fields['key']).to eq('obj-key')
          expect(policy(post)).to include('key' => 'obj-key')
        end

        it 'can be set via :key_starts_with' do
          options[:key_starts_with] = 'prefix/'
          expect(post.fields['key']).to be(nil)
          expect(policy(post)).to include(['starts-with', '$key', 'prefix/'])
        end

        it 'can be set via #key' do
          post.key('obj-key')
          expect(post.fields['key']).to eq('obj-key')
          expect(policy(post)).to include('key' => 'obj-key')
        end

        it 'can be set via #key_starts_with' do
          post.key_starts_with('prefix/')
          expect(post.fields['key']).to be(nil)
          expect(policy(post)).to include(['starts-with', '$key', 'prefix/'])
        end

        it 'can be set via :allow_any' do
          options[:allow_any] = 'key'
          expect(post.fields['key']).to be(nil)
          expect(policy(post)).to include(['starts-with', '$key', ''])
        end

        it 'can be set via #allow_any' do
          post.allow_any('key')
          expect(post.fields['key']).to be(nil)
          expect(policy(post)).to include(['starts-with', '$key', ''])
        end
      end

      describe ':signature_expiration' do
        it 'defaults to one hour from now' do
          now = Time.now
          allow(Time).to receive(:now).and_return(now)
          policy = decode(post.key('key').fields['policy'])
          expect(policy['expiration']).to eq((now + 3600).utc.iso8601)
        end

        it 'can be set in the constructor' do
          now = Time.now
          options[:signature_expiration] = now
          policy = decode(post.key('key').fields['policy'])
          expect(policy['expiration']).to eq(now.utc.iso8601)
        end
      end

      describe 'fields' do
        before(:each) do
          post.key('key')
        end

        it 'provides methods for setting fields or fields starts with' do
          post = PresignedPost.new(creds, region, bucket).key('key')
          post.acl('public-read')
          expect(post.fields['acl']).to eq('public-read')
          expect(policy(post)).to include('acl' => 'public-read')

          post = PresignedPost.new(creds, region, bucket).key('key')
          post.acl_starts_with('')
          expect(post.fields['acl']).to be(nil)
          expect(policy(post)).to include(['starts-with', '$acl', ''])
        end

        it 'formats the Expires time value as httpdate' do
          now = Time.now
          post.expires(now)
          expect(post.fields['Expires']).to eq(now.httpdate)
        end

        it 'allows prefixed Expires' do
          post.expires_starts_with('')
          expect(post.fields['Expires']).to be(nil)
          expect(policy(post)).to include(['starts-with', '$Expires', ''])
        end

        it 'accepts a range for content_length_range' do
          post.content_length_range(10..20)
          expect(policy(post)).to include(['content-length-range', 10, 20])
        end

        it 'respects non-inclusive ranges' do
          post.content_length_range(10...20)
          expect(policy(post)).to include(['content-length-range', 10, 19])
        end

        it 'accepts a hash to :metadata' do
          post.metadata(foo: 'bar', 'mno' => 'xyz')
          expect(post.fields['x-amz-meta-foo']).to eq('bar')
          expect(post.fields['x-amz-meta-mno']).to eq('xyz')
          expect(policy(post)).to include('x-amz-meta-foo' => 'bar')
          expect(policy(post)).to include('x-amz-meta-mno' => 'xyz')
        end

        it 'accepts a hash to :metadata_starts_with' do
          post.metadata_starts_with(foo: 'foo/', 'mno' => 'mno/')
          expect(post.fields['x-amz-meta-foo']).to be(nil)
          expect(post.fields['x-amz-meta-mno']).to be(nil)
          expect(policy(post)).to include(
            ['starts-with', '$x-amz-meta-foo', 'foo/']
          )
          expect(policy(post)).to include(
            ['starts-with', '$x-amz-meta-mno', 'mno/']
          )
        end

        it 'computes a MD5 of the customer provided encryption key' do
          key = 'abcmnoxyz12345'
          encoded_key = Base64.strict_encode64(key)
          md5 = Base64.strict_encode64(OpenSSL::Digest::MD5.digest(key))
          post.server_side_encryption_customer_key(key)
          expect(
            post.fields['x-amz-server-side-encryption-customer-key']
          ).to eq(encoded_key)
          expect(
            post.fields['x-amz-server-side-encryption-customer-key-MD5']
          ).to eq(md5)
          expect(policy(post)).to include(
            'x-amz-server-side-encryption-customer-key' => encoded_key
          )
          expect(policy(post)).to include(
            'x-amz-server-side-encryption-customer-key-MD5' => md5
          )
        end

        it 'does not computes MD5 for starts with' do
          post.server_side_encryption_customer_key_starts_with('prefix')
          expect(
            post.fields['x-amz-server-side-encryption-customer-key']
          ).to be(nil)
          expect(
            post.fields['x-amz-server-side-encryption-customer-key-MD5']
          ).to be(nil)
          expect(policy(post)).to include(
            [
              'starts-with', '$x-amz-server-side-encryption-customer-key',
              'prefix'
            ]
          )
        end

        it 'accepts ${filename} to eq' do
          post.metadata('orig-filename' => '${filename}')
          expect(post.fields['x-amz-meta-orig-filename']).to eq('${filename}')
          expect(policy(post)).to include(
            ['starts-with', '$x-amz-meta-orig-filename', '']
          )
        end

        it 'accepts ${filename} and removes it from starts-with' do
          post = PresignedPost.new(creds, region, bucket)
          post.key('prefix/${filename}')
          expect(post.fields['key']).to eq('prefix/${filename}')
          expect(policy(post)).to include(['starts-with', '$key', 'prefix/'])
        end

        it 'requires ${filename} to be at the end of the value' do
          post = PresignedPost.new(creds, region, bucket)
          expect do
            post.key('${filename}/foo')
          end.to raise_error(ArgumentError)
        end

        it 'accepts a list of fields to white-list' do
          post.allow_any(['foo']).allow_any('Filename')
          expect(post.fields['foo']).to be(nil)
          expect(post.fields['Filename']).to be(nil)
          expect(policy(post)).to include(['starts-with', '$foo', ''])
          expect(policy(post)).to include(['starts-with', '$Filename', ''])
        end
      end

      describe '#fields' do
        it 'returns a hash with a policy document and signature' do
          now = Time.now
          allow(Time).to receive(:now).and_return(now)
          post.key('key')
          expect(post.fields.keys).to eq(
            [
              'key', 'policy', 'x-amz-credential', 'x-amz-algorithm',
              'x-amz-date', 'x-amz-signature'
            ]
          )
          expect(post.fields['x-amz-algorithm']).to eq('AWS4-HMAC-SHA256')
          expect(post.fields['x-amz-credential']).to eq(
            "akid/#{now.strftime('%Y%m%d')}/#{region}/s3/aws4_request"
          )
          expect(post.fields['x-amz-date']).to eq(
            now.strftime('%Y%m%dT%H%M%SZ')
          )
        end

        it 'generates a valid signature' do
          # test example from http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
          creds = Credentials.new(
            'AKIAIOSFODNN7EXAMPLE',
            'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
          )
          region = 'us-east-1'
          bucket = 'examplebucket'
          now = Time.parse('20130806T000000Z')
          allow(Time).to receive(:now).and_return(now)
          post = PresignedPost.new(creds, region, bucket, key: 'key')
          policy = <<-POLICY.strip
            eyAiZXhwaXJhdGlvbiI6ICIyMDEzLTA4LTA3VDEyOjAwOjAwLjAwMFoiLA0KICAiY29uZGl0aW9ucyI6IFsNCiAgICB7ImJ1Y2tldCI6ICJleGFtcGxlYnVja2V0In0sDQogICAgWyJzdGFydHMtd2l0aCIsICIka2V5IiwgInVzZXIvdXNlcjEvIl0sDQogICAgeyJhY2wiOiAicHVibGljLXJlYWQifSwNCiAgICB7InN1Y2Nlc3NfYWN0aW9uX3JlZGlyZWN0IjogImh0dHA6Ly9leGFtcGxlYnVja2V0LnMzLmFtYXpvbmF3cy5jb20vc3VjY2Vzc2Z1bF91cGxvYWQuaHRtbCJ9LA0KICAgIFsic3RhcnRzLXdpdGgiLCAiJENvbnRlbnQtVHlwZSIsICJpbWFnZS8iXSwNCiAgICB7IngtYW16LW1ldGEtdXVpZCI6ICIxNDM2NTEyMzY1MTI3NCJ9LA0KICAgIFsic3RhcnRzLXdpdGgiLCAiJHgtYW16LW1ldGEtdGFnIiwgIiJdLA0KDQogICAgeyJ4LWFtei1jcmVkZW50aWFsIjogIkFLSUFJT1NGT0ROTjdFWEFNUExFLzIwMTMwODA2L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QifSwNCiAgICB7IngtYW16LWFsZ29yaXRobSI6ICJBV1M0LUhNQUMtU0hBMjU2In0sDQogICAgeyJ4LWFtei1kYXRlIjogIjIwMTMwODA2VDAwMDAwMFoiIH0NCiAgXQ0KfQ==
          POLICY
          allow(post).to receive(:policy).and_return(policy)
          expect(post.fields['x-amz-signature']).to eq(
            '21496b44de44ccb73d545f1a995c68214c9cb0d41c45a17a5daeec0b1a6db047'
          )
        end

        it 'adds x-amz-security-token as appropriate' do
          creds = Credentials.new('key', 'secret', 'token')
          post = PresignedPost.new(creds, region, bucket).key('key')
          expect(post.fields['x-amz-security-token']).to eq('token')
          expect(policy(post)).to include('x-amz-security-token' => 'token')
        end

        it 'sets the storage class if requested' do
          creds = Credentials.new('key', 'secret', 'token')
          post = PresignedPost.new(
            creds,
            region,
            bucket
          ).key('key').storage_class('REDUCED_REDUNDANCY')
          expect(post.fields['x-amz-storage-class']).to eq('REDUCED_REDUNDANCY')
        end
      end
    end
  end
end
