The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

use strict;
use warnings;

use Test::More;
use t::TestHTTP;

use IO::Async::Test;
use IO::Async::Loop;

use HTTP::Response;

use Net::Async::Webservice::S3;

my $loop = IO::Async::Loop->new;
testing_loop( $loop );

my $s3 = Net::Async::Webservice::S3->new(
   max_retries => 1,
   http => my $http = TestHTTP->new,
   access_key => 'K'x20,
   secret_key => 's'x40,
);

$loop->add( $s3 );

# Simple get
{
   my $f = $s3->get_object(
      bucket => "bucket",
      key    => "one",
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   is( $req->method,         "GET",                     'Request method' );
   is( $req->uri->authority, "bucket.s3.amazonaws.com", 'Request URI authority' );
   is( $req->uri->path,      "/one",                    'Request URI path' );

   $http->respond(
      HTTP::Response->new( 200, "OK", [
         Content_Type => "text/plain",
      ], <<'EOF' )
Here is the key
EOF
   );

   wait_for { $f->is_ready };

   my ( $value, $response ) = $f->get;
   is( $value, "Here is the key\n", '$value for simple get' );
   is( $response->content_type, "text/plain", '$response->content_type for simple get' );
}

# Streaming get
{
   my ( $header, $bytes );
   my $f = $s3->get_object(
      bucket => "bucket",
      key    => "one",
      on_chunk => sub {
         ( $header, $bytes ) = @_;
      },
   );

   wait_for { $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   $http->respond_header(
      HTTP::Response->new( 200, "OK", [
         Content_Type => "text/plain",
      ], "" )
   );

   $http->respond_more( "some bytes" );

   wait_for { defined $header };
   is( $header->content_type, "text/plain", '$header->content_type for chunked get' );
   is( $bytes, "some bytes", '$bytes for chunked get' );

   $http->respond_done;

   wait_for { $f->is_ready };
}

# Get byte range
{
   my $f = $s3->get_object(
      bucket => "bucket",
      key    => "one",
      byte_range => "8-",
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   is( $req->method,         "GET",                     'Request method' );
   is( $req->uri->authority, "bucket.s3.amazonaws.com", 'Request URI authority' );
   is( $req->uri->path,      "/one",                    'Request URI path' );
   is( $req->header( "Range" ), "bytes=8-",             'Request Range header' );

   $http->respond(
      HTTP::Response->new( 206, "Partial Content", [
         Content_Type => "text/plain",
         Content_Range => "bytes 8-15/16",
      ], <<'EOF' )
the key
EOF
   );

   wait_for { $f->is_ready };

   my ( $value, $response ) = $f->get;
   is( $value, "the key\n", '$value for ranged get' );
   is( $response->content_type, "text/plain", '$response->content_type for ranged get' );
}

# Get with If-Match
{
   my $f = $s3->get_object(
      bucket => "bucket",
      key    => "one",
      if_match => '"my-etag-here"',
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   is( $req->method,         "GET",                     'Request method' );
   is( $req->uri->authority, "bucket.s3.amazonaws.com", 'Request URI authority' );
   is( $req->uri->path,      "/one",                    'Request URI path' );
   is( $req->header( "If-Match" ), '"my-etag-here"',    'Request If-Match header' );

   $http->respond(
      HTTP::Response->new( 412, "Precondition Failed", [], "" ),
   );

   wait_for { $f->is_ready };

   ok( scalar $f->failure, '$f fails after 412 error' );
   my ( $failure, $name, $resp ) = $f->failure;
   is( $resp->code, 412, 'failure request has ->code 412' );
}

# Get with implied bucket and prefix
{
   $s3->configure(
      bucket => "bucket",
      prefix => "subdir/",
   );

   my $f = $s3->get_object(
      key => "1",
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   is( $req->method,         "GET",                     'Request method' );
   is( $req->uri->authority, "bucket.s3.amazonaws.com", 'Request URI authority' );
   is( $req->uri->path,      "/subdir/1",               'Request URI path' );

   $http->respond(
      HTTP::Response->new( 200, "OK", [
         Content_Type => "text/plain",
      ], <<'EOF' )
More content
EOF
   );

   wait_for { $f->is_ready };
   $f->get;

   $s3->configure(
      bucket => undef,
      prefix => undef,
   );
}

# Get with metadata
{
   my $f = $s3->get_object(
      bucket => "bucket",
      key    => "ONE",
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   $http->respond(
      HTTP::Response->new( 200, "OK", [
         Content_Type => "text/plain",
         'X-Amz-Meta-One' => "one",
      ], <<'EOF' )
value
EOF
   );

   wait_for { $f->is_ready };

   my ( $value, $response, $meta ) = $f->get;
   is_deeply( $meta, { One => "one" }, '$meta for get with metadata' );
}

# get error
{
   my $f = $s3->get_object(
      bucket => "bucket",
      key    => "five",
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   $http->respond(
      HTTP::Response->new( 404, "Not Found", [], '' )
   );

   wait_for { $f->is_ready };

   ok( scalar $f->failure, '$f fails after 404 error' );
   my ( $failure, $name, $resp ) = $f->failure;
   is( $resp->code, 404, 'failure request has ->code 404' );
}

# Simple head
{
   my $f = $s3->head_object(
      bucket => "bucket",
      key    => "one",
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   is( $req->method,         "HEAD",                    'Request method' );
   is( $req->uri->authority, "bucket.s3.amazonaws.com", 'Request URI authority' );
   is( $req->uri->path,      "/one",                    'Request URI path' );

   $http->respond(
      HTTP::Response->new( 200, "OK", [
         Content_Type => "text/plain",
      ], '' )
   );

   wait_for { $f->is_ready };

   my ( $response ) = $f->get;
   is( $response->content_type, "text/plain", '$response->content_type for simple get' );
}

# head error
{
   my $f = $s3->head_object(
      bucket => "bucket",
      key    => "five",
   );

   my $req;
   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   $http->respond(
      HTTP::Response->new( 404, "Not Found", [], '' )
   );

   wait_for { $f->is_ready };

   ok( scalar $f->failure, '$f fails after 404 error' );
   my ( $failure, $name, $resp ) = $f->failure;
   is( $resp->code, 404, 'failure request has ->code 404' );
}

# head_then_get
{
   my $head_f = $s3->head_then_get_object(
      bucket => "bucket",
      key    => "one",
   );

   my $req;
   wait_for { $req = $http->pending_request or $head_f->is_ready };
   $head_f->get if $head_f->is_ready and $head_f->failure;

   is( $req->method,         "GET",                     'Request method' );
   is( $req->uri->authority, "bucket.s3.amazonaws.com", 'Request URI authority' );
   is( $req->uri->path,      "/one",                    'Request URI path' );

   $http->respond_header(
      HTTP::Response->new( 200, "OK", [
         Content_Type => "text/plain",
         'X-Amz-Meta-One' => "one",
      ], "" )
   );
   $http->respond_more( "And now here is some content\n" );

   wait_for { $head_f->is_ready };

   my ( $value_f, $header, $meta ) = $head_f->get;

   ok( !$value_f->is_ready, '$value_f is not yet ready before sending body content' );
   is( $header->content_type, "text/plain", '$header->content_type for head_then_get' );
   is_deeply( $meta, { One => "one" }, '$meta for head_then_get' );

   $http->respond_done;

   wait_for { $value_f->is_ready };

   my ( $value, $header_2, $meta_2 ) = $value_f->get;
   is( $value, "And now here is some content\n", '$value after completion' );
   is( $header_2, $header, '$header again from value future' );
   is_deeply( $meta_2, $meta, '$meta again from value future' );
}

# head_then_get error
{
   my $head_f = $s3->head_then_get_object(
      bucket => "bucket",
      key    => "five",
   );

   my $req;
   wait_for { $req = $http->pending_request or $head_f->is_ready };
   $head_f->get if $head_f->is_ready and $head_f->failure;

   $http->respond(
      HTTP::Response->new( 404, "Not Found", [], "Object was not found" )
   );

   wait_for { $head_f->is_ready };

   ok( scalar $head_f->failure, '$head_f fails after 404 error' );
   my ( $failure, $name, $resp ) = $head_f->failure;
   is( $resp->code, 404, 'failure request has ->code 404' );
}

# head_then_get cancellation
{
   my $head_f = $s3->head_then_get_object(
      bucket => "bucket",
      key    => "one",
   );

   my $req;
   wait_for { $req = $http->pending_request or $head_f->is_ready };
   $head_f->get if $head_f->is_ready and $head_f->failure;

   $head_f->cancel;

   $http->respond_header(
      HTTP::Response->new( 200, "OK", [
         Content_Type => "text/plain",
         'X-Amz-Meta-One' => "one",
      ], "" )
   );
   $http->respond_more( "And now here is some content\n" );
   $http->respond_done;

   $head_f = $s3->head_then_get_object(
      bucket => "bucket",
      key    => "one",
   );

   undef $req;
   wait_for { $req = $http->pending_request or $head_f->is_ready };
   $head_f->get if $head_f->is_ready and $head_f->failure;

   $head_f->cancel;

   $http->respond(
      HTTP::Response->new( 404, "Not Found", [], "" )
   );
}

# Test that timeout argument is set only for direct argument
{
   my $f;
   my $req;

   $s3->configure( timeout => 10 );
   $f = $s3->get_object(
      bucket => "bucket",
      key    => "-one-",
   );

   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   is( $req->header( "X-NaHTTP-Timeout" ), undef, 'Request has no timeout for configured' );
   $http->respond( HTTP::Response->new( 200, "OK", [] ) );

   $f = $s3->get_object(
      bucket  => "bucket",
      key     => "-one-",
      timeout => 20,
   );

   wait_for { $req = $http->pending_request or $f->is_ready };
   $f->get if $f->is_ready and $f->failure;

   is( $req->header( "X-NaHTTP-Timeout" ), 20, 'Request has timeout set for immediate' );
   $http->respond( HTTP::Response->new( 200, "OK", [] ) );
}

done_testing;