API-Docker
view release on metacpan or search on metacpan
t/role_http.t view on Meta::CPAN
subtest 'the streaming reader skips a 1xx before the stream too' => sub {
my @got;
my $handler = $client->_stream_handler('GET /v1.41/events', 'on_event',
sub { push @got, $_[0] }, 0);
my $raw = "HTTP/1.1 100 Continue\r\n\r\n"
. "HTTP/1.1 200 OK\r\n"
. "Transfer-Encoding: chunked\r\n\r\n"
. qq(11\r\n{"status":"one"}\n\r\n)
. "0\r\n\r\n";
my $res = $client->_read_streaming_response(
string_handle($raw), 'GET', $handler, {});
is $res->[0], 200, 'the stream reader also passes the 100 by';
is_deeply [ map { $_->{status} } @got ], ['one'],
'and the real event reaches the callback';
};
# ---------------------------------------------------------------------------
subtest '_read_chunked: hex sizes, upper and lower case' => sub {
# 'a' and 'A' are both 10 -- hex() is case-insensitive, and so must this be.
my $raw = "a\r\n0123456789\r\nA\r\nABCDEFGHIJ\r\n0\r\n\r\n";
my $fh = string_handle($raw);
is $client->_read_chunked($fh), '0123456789ABCDEFGHIJ',
'lowercase and uppercase hex chunk sizes both read correctly';
};
subtest '_read_chunked: a single zero-size chunk terminates immediately' => sub {
my $fh = string_handle("0\r\n\r\n");
is $client->_read_chunked($fh), '', 'empty body, no chunks';
};
subtest '_read_chunked: a chunk arriving in several reads' => sub {
my $data = "b\r\nhello world\r\n0\r\n\r\n"; # 'b' hex = 11 = length("hello world")
tie *FH, 'Test::RoleHTTP::PartialReader', $data, 3; # 3 bytes per read() call
my $body = $client->_read_chunked(\*FH);
is $body, 'hello world',
'chunk payload reassembled correctly across multiple short reads';
untie *FH;
};
# ---------------------------------------------------------------------------
subtest '_uri_encode: what it escapes and what it leaves alone' => sub {
# Called as a bare function everywhere in the module (see _request's
# query-string assembly) -- not as a method. Calling it as $client->
# _uri_encode(...) would silently shift $client into the $str slot, since
# the sub only unpacks a single positional argument.
my $encode = \&API::Docker::Role::HTTP::_uri_encode;
is $encode->('alpine:latest'), 'alpine:latest',
'colon is left raw -- image references keep their tag separator';
is $encode->('myrepo/app:v1'), 'myrepo/app:v1',
'slash is left raw too -- image references keep their path shape';
is $encode->('abcXYZ019-_.~'), 'abcXYZ019-_.~',
'unreserved characters (alnum - _ . ~) are never escaped';
is $encode->('a b'), 'a%20b', 'space is percent-encoded';
is $encode->('foo?bar=baz'), 'foo%3Fbar%3Dbaz',
'? and = are percent-encoded';
is $encode->('100%'), '100%25', 'a literal percent sign is escaped itself';
is $encode->("a\nb"), 'a%0Ab', 'control characters are escaped, not passed through';
# A character string -- what a name/tag/author/comment/search term arrives as
# under `use utf8` or through a :utf8 layer -- is escaped by its UTF-8 bytes,
# not by its codepoint. The old code took ord() of the character, so 'ü'
# became %FC (not even valid UTF-8) and 'ä¸' became %4E2D.
is $encode->("\x{4E2D}"), '%E4%B8%AD',
'a wide character is escaped by its UTF-8 bytes, not its codepoint';
{
my $u = "\x{00FC}";
utf8::upgrade($u); # what a decoded 'ü' is: codepoint 252, the utf8 flag on
is $encode->($u), '%C3%BC',
'a Latin-1 character with the utf8 flag is UTF-8 encoded before escaping';
}
# The other half, and the reason the encoding is not unconditional: a byte
# string is already octets and must be escaped as-is. encode_json hands a
# HASH param (filters among them) its UTF-8 bytes, and re-encoding those would
# turn %C3%BC into %C3%83%C2%BC -- trading this bug for a broader one.
is $encode->("\xC3\xBC"), '%C3%BC',
'a byte string of UTF-8 octets is escaped as-is, never double-encoded';
};
# ---------------------------------------------------------------------------
subtest '_request: assembles the request line, headers and body' => sub {
my $t = Test::API::Docker::FakeTransport->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
subtest 'plain GET, no body' => sub {
$t->_request('GET', '/containers/json');
my $req = $t->written;
like $req, qr{\AGET /v1\.41/containers/json HTTP/1\.1\r\n},
'method, versioned path, and protocol on the request line';
like $req, qr{Host: localhost\r\n}, 'Host header sent';
like $req, qr{Connection: close\r\n}, 'Connection: close sent';
like $req, qr{User-Agent: API-Docker\r\n}, 'User-Agent sent';
unlike $req, qr{Content-Type}, 'no Content-Type without a body';
unlike $req, qr{Content-Length}, 'no Content-Length without a body';
like $req, qr{\r\n\r\n\z}, 'request ends on the blank line, empty body';
};
subtest 'POST with a JSON body' => sub {
$t->_request('POST', '/containers/create', body => { Image => 'alpine:3' });
my $req = $t->written;
my $encoded = encode_json({ Image => 'alpine:3' });
like $req, qr{\APOST /v1\.41/containers/create HTTP/1\.1\r\n},
'request line for the POST';
like $req, qr{Content-Type: application/json\r\n}, 'JSON content type';
like $req, qr{Content-Length: @{[ length $encoded ]}\r\n},
'content-length matches the encoded body';
like $req, qr{\r\n\r\n\Q$encoded\E\z}, 'body follows the blank line verbatim';
};
subtest 'raw_body + content_type (tarball upload)' => sub {
my $tar = "fake tar bytes\0\0\0";
$t->_request('POST', '/build', raw_body => $tar, content_type => 'application/x-tar');
my $req = $t->written;
like $req, qr{Content-Type: application/x-tar\r\n},
'content type overridden for a raw body, not left as application/json';
like $req, qr{Content-Length: @{[ length $tar ]}\r\n},
'content-length matches the raw body, not a JSON encoding of it';
like $req, qr{\r\n\r\n\Q$tar\E\z}, 'raw bytes appended verbatim';
};
subtest 'params: sorted, hashref values JSON-encoded, then URI-encoded' => sub {
$t->_request('GET', '/images/json',
params => { all => 1, filters => { dangling => ['true'] } });
my $req = $t->written;
my ($request_line) = $req =~ /\A(GET [^\r\n]+)\r\n/;
my $expected_filters = API::Docker::Role::HTTP::_uri_encode(
encode_json({ dangling => ['true'] }));
( run in 1.773 second using v1.01-cache-2.11-cpan-364913b4093 )