view release on metacpan or search on metacpan
.claude/agents/api-docker-test-writer.md view on Meta::CPAN
the NDJSON fallback â is invisible to a route-table test. Transport behavior is tested
either by calling the private function directly (`t/images_push_auth.t` calls
`_build_registry_auth_header`) or by capturing `local *API::Docker::_request`. State
which level you are on before writing the file.
- **`API::Docker::Role::HTTP` currently has no coverage beyond `use_ok`.**
`_read_chunked`, `_read_response` and the >=400 croak path are untested; a fake socket
(an in-memory filehandle over a canned HTTP/1.1 response) is the way in. Treat that as
a standing gap worth a ticket, not as something to fix inside an unrelated task.
- **Route keys are matched as exact strings first, then as regexes** (`m{^$route_path$}`
in the fallback). A key containing `.`, `?` or `+` matches more than it looks like it
does â anchor intent by making the exact key match, or escape deliberately.
- **Assert the request, not only the response.** A route handler receives
`($method, $clean_path, %opts)`: assert on `params`, `body` and `headers` there when
the point of the test is what the client sends. A test that only checks the mocked
return value proves the fixture, not the code.
- **Decode exactly what the engine would receive.** The push-auth helper used to append
the missing base64 padding before decoding and so passed with and without the defect
it existed to catch. Never normalise the value under test on the way into the
assertion.
- **Live and mock must both be able to pass, or the assertion is gated.** `test_docker`
ignores the route table entirely under `API_DOCKER_TEST_HOST` â an assertion tied to
.claude/skills/api-docker-core/SKILL.md view on Meta::CPAN
recommended, not required, dependency, loaded only once a TLS connection is
actually opened. Detail: `API::Docker::Role::HTTP`'s "TLS on a tcp://
connection".
- **No connection reuse.** Each `_request` calls `_reconnect` and closes
afterwards, streamed or not.
## Tests â `Test::API::Docker::Mock`
`test_docker('GET /images/json' => $fixture_or_coderef, ...)` returns a client
whose `_request` dispatches against the route table (exact key first, then the
key matched as a literal path -- it is `\Q..\E`-escaped, not a regex, so a
metacharacter in a route key means itself). A `GET /version` route is injected
when none is given.
**In live mode `test_docker` ignores the routes entirely** and returns a real
client against `$ENV{API_DOCKER_TEST_HOST}`. An assertion that only holds for
the fixture must sit behind `is_live()`; mutating tests behind `can_write()` /
`skip_unless_write()`, with `register_cleanup` for anything they create.
Fixtures in `t/fixtures/*.json` are captured from a real daemon, so drift stays
detectable â do not hand-roll them. That was not always true until karr k101
.claude/skills/api-docker-type-model/references/types.md view on Meta::CPAN
[Str] an array of scalars
[[Str]] an array of arrays of scalars
['PortBinding'] an array of typed objects
'HostConfig' a single typed object
{ Str, Str } a hash whose KEYS ARE CALLER DATA
{ Str, ['PortBinding'] } same, values are typed
Docker's `definitions:` are flat â there are no groups and no prefix map. A
quoted short name is expanded under `API::Docker::Type::`, and an inline
object nests under its owner (`Mount::BindOptions`). `+Full::Class::Name`
escapes the expansion.
The swagger's `allOf` is not a type but inheritance: `docker_extends 'Resources'`
at the top of the class, which merges the parent's registry entries first so
serialisation keeps the swagger's own field order.
## Keys that are caller data â never translate these
The hash form `{ Str, ... }` marks a field whose keys the user chose. The DSL
must pass those keys through byte for byte. Getting this wrong silently
rewrites user input.
.claude/skills/getty-perl-core/SKILL.md view on Meta::CPAN
## Errors
- **`croak`, never `die`.** Errors report the caller's line, not ours.
- **Import it:** `use Carp qw( croak );` and call `croak(...)` bare.
- **Name the origin in the message:** `croak __PACKAGE__."->state too many args"` â or whatever identifies the operation in that module's DSL.
## Strings
- **Concatenate, do not interpolate:** `'Adding '.$f.' with '.$length.' bytes'`. Interpolate only where concatenation would be unreadable.
- **Single quotes by default.** `'...'` and `"..."` are genuinely different in Perl â `"` interpolates and processes escapes, `'` does not. Reach for `"` when you need that, not by habit.
- **Import lists as `qw( croak confess )`** â spaces inside the parens. Never rely on default exports.
## Control flow
- **Postfix `if`/`unless`** for guards and short conditions: `croak(...) if $self->readonly;`
- **`unless $x`** instead of `if !$x`.
- **Guard clauses return bare:** `return unless $res->is_success;` â not `return undef;`.
- **Nested ternaries** for a return that picks between expressions, instead of an if/elsif chain.
## Data
lib/API/Docker/API/Distribution.pm view on Meta::CPAN
=head1 DESCRIPTION
This module provides access to the Docker distribution endpoint
(C<GET /distribution/{name}/json>), which asks a I<registry> for the manifest
descriptor of an image reference without pulling the image.
Accessed via C<< $docker->distribution >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->distribution->using(read_timeout => 5) >>.
The reference goes into the path unescaped, so its slashes and its tag stay
readable on the wire (C</distribution/myrepo/app:1.0/json>) -- that is what
the engine parses, and percent-encoding them breaks the reference.
=head2 A 404 means two different things
The endpoint answers 404 both when the registry does not have the reference
and when the engine has no such route, and the two want opposite handling.
The split here is:
=over
lib/API/Docker/API/Distribution.pm view on Meta::CPAN
that cannot fail loudly would have reintroduced it one layer up.
=head2 Not available on Podman
Measured against the rootless Podman socket (5.4.2, API 1.41):
C<< GET /v1.41/distribution/nginx:latest/json >> answers C<404 Not Found>
with
C<< {"cause":"","message":"Path /v1.41/distribution/nginx:latest/json is not supported","response":0} >>
(the C<1.41> there is this client's negotiated API version, echoed back from
the request path -- it moves with negotiation, not a fixed string),
and so does every other reference, escaped or not -- the compat layer has no
route for this endpoint. This class therefore needs a real Docker daemon.
That 404 is exactly the one a naive predicate would read as "the registry
does not have it", which is why L</exists> tells the engine's own
no-such-route answer apart and croaks on it instead.
=head2 What this class returns
L</inspect> returns the decoded engine response -- a HashRef with
C<Descriptor> and C<Platforms> -- not an entity object, deviating from the
lib/API/Docker/API/Images.pm view on Meta::CPAN
. 'of names; got an odd number of them' if @names % 2;
%opts = @names;
@names = @$list;
}
croak "At least one image name required" unless @names;
# `names` is a repeated query parameter -- names=a&names=b -- and nothing
# else is accepted: measured against Podman 5.4.2, the comma-joined spelling
# answers 500 with 'parsing reference "alpine:3,registry:2": invalid
# reference format'. An ArrayRef param value is exactly that repetition;
# _request escapes each element with its own _uri_encode, which leaves `/`
# and `:` raw so an image reference survives intact.
return $self->client->get('/images/get', params => { names => \@names },
%{ $self->_request_options },
exists $opts{on_chunk} ? ( on_chunk => $opts{on_chunk} ) : ( raw => 1 ));
}
sub load {
my ($self, $tar, %opts) = @_;
croak "Tar archive required (raw bytes or a scalar ref)" unless defined $tar;
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
say $plugin->enabled;
say join ', ', @{ $plugin->settings->env };
Get detailed information about an installed plugin. Returns an
L<API::Docker::Type::Plugin> -- the same class L</list> returns; see
L</"What this class returns">.
The name may carry a registry host, a repository path and a tag
(C<docker.io/vieux/sshfs:latest>) and is interpolated into the request path
as given: the daemon routes this endpoint as C<< /plugins/{name:.*}/json >>,
so the slashes and the colon must survive unescaped, and they do.
=head2 remove
$plugins->remove('vieux/sshfs:latest');
$plugins->remove('vieux/sshfs:latest', force => 1);
Remove an installed plugin. A plugin that is still enabled is refused unless
C<force> is set.
Options:
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
$self->_croak_truncated($ctx, phase => 'content-length',
detail => "the Content-Length header '" . $value . "' is not a number");
}
sub _uri_encode {
my ($str) = @_;
# Escape a character string by its UTF-8 bytes ('ü' -> %C3%BC, not %FC), and
# a byte string as it stands. ord() on a character is not its wire byte: a
# name or tag typed under `use utf8`, or read through a :utf8 layer, arrives
# as characters and used to escape to a lone high byte or a bare codepoint
# (%FC, %4E2D) that is not UTF-8 at all. But the encoding cannot be
# unconditional: encode_json has already handed a HASH param (filters among
# them) its UTF-8 octets, and re-encoding those would double them
# (%C3%BC -> %C3%83%C2%BC). The utf8 flag is exactly that distinction -- on
# for a decoded string, off for encode_json's output -- so a copy is encoded
# only when it carries one, leaving the caller's own value untouched either
# way.
my $bytes = $str;
utf8::encode($bytes) if utf8::is_utf8($bytes);
$bytes =~ s/([^A-Za-z0-9\-_.~:\/])/sprintf("%%%02X", ord($1))/ge;
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
catches the separators that would corrupt the request without injecting
anything.
=head2 A request path is rejected, not sanitised
The C<$path> given to L</get>, L</post>, L</put>, L</delete_request>, L</head>
and C<_request> is spliced straight into the request line as
C<< $method /v$version$path HTTP/1.1 >>, and it carries caller data: the
resource methods build it by interpolation -- C<< "/containers/$id/json" >>,
C<< "/images/$name/push" >> -- so a container name or an image reference the
user typed ends up in the request line unescaped. A byte the line's own
grammar reads therefore rewrites the request rather than naming a resource: a
CR or LF ends the line and opens a header of its own, a space starts the
HTTP-version field, and a C<?> or C<#> opens the query string or fragment.
So the path is checked against the RFC 3986 origin-form character set --
unreserved, the sub-delims, and C<:> C<@> C<%> C<< / >>, which is the set an
image reference lives in -- and a path outside it is refused with a croak
before anything reaches the wire, the same treatment and for the same reason a
header name gets. Sanitising is not on the table here: percent-encoding the
path at this layer cannot tell a separator from data, so it would either
lib/API/Docker/Type.pm view on Meta::CPAN
[[Str]] an array of arrays of scalars
['PortBinding'] an array of typed objects
'PortBinding' a single typed object
'+Some::Other::Class' the same, without the namespace prefix
{ Str, Str } a hash whose KEYS ARE CALLER DATA
{ Str, ['PortBinding'] } the same, with typed values
A bare class name is short: C<'PortBinding'> is
C<API::Docker::Type::PortBinding>, C<'Mount::BindOptions'> is
C<API::Docker::Type::Mount::BindOptions>. The expansion happens in
C<_expand_class> and nowhere else; a leading C<+> escapes it.
=head2 describe_type
API::Docker::Type::describe_type($info->{type}); # 'hash<array<object>>'
A descriptor as one string, for the drift checker's report. Objects render
as C<< object<Class> >>.
=head1 SUPPORT
lib/API/Docker/Type/ContainerConfig.pm view on Meta::CPAN
docker env => [Str];
docker cmd => [Str];
docker healthcheck => 'HealthConfig';
docker args_escaped => Bool;
docker image => Str;
docker volumes => { Str, Any };
docker working_dir => Str;
lib/API/Docker/Type/ContainerConfig.pm view on Meta::CPAN
=head2 cmd
Command to run specified as a string or an array of strings.
=head2 healthcheck
A test to perform to check that the container is healthy. See
L<API::Docker::Type::HealthConfig>.
=head2 args_escaped
Command is already escaped (Windows only). The daemon defaults it to false.
=head2 image
The name (or reference) of the image to use when creating the container, or
which was used when the container was created.
=head2 volumes
An object mapping mount point paths inside the container to empty objects.
B<The keys are the caller's data> and are never translated.
lib/API/Docker/Type/ImageConfig.pm view on Meta::CPAN
docker env => [Str];
docker cmd => [Str];
docker healthcheck => 'HealthConfig';
docker args_escaped => Bool;
docker volumes => { Str, Any };
docker working_dir => Str;
docker entrypoint => [Str];
lib/API/Docker/Type/ImageConfig.pm view on Meta::CPAN
=head2 cmd
Command to run specified as a string or an array of strings.
=head2 healthcheck
A test to perform to check that the container is healthy. See
L<API::Docker::Type::HealthConfig>.
=head2 args_escaped
Command is already escaped (Windows only). The daemon defaults it to false.
=head2 volumes
An object mapping mount point paths inside the container to empty objects.
B<The keys are the caller's data> and are never translated.
=head2 working_dir
The working directory for commands to run in.
t/distribution.t view on Meta::CPAN
# is_live() except the one subtest that says so. Test::API::Docker::Mock is
# deliberately not used for the bulk of this file: under API_DOCKER_TEST_HOST
# it ignores its route table and returns a real client, and the only engine
# reachable here is Podman, which serves no route for this endpoint at all --
# measured, rootless Podman 5.4.2 (API 1.41):
#
# GET /v1.41/distribution/nginx:latest/json
# -> 404 {"cause":"","message":"Path /v1.41/distribution/nginx:latest/json
# is not supported","response":0}
#
# and the same for a bare name and for a percent-escaped reference. That
# version number just echoes the requested path prefix, not a fixed daemon
# constant -- re-measured live on 5.8.4 / API 1.44 the same request gets
# "/v1.44/distribution/..." back instead (karr k62). $PODMAN_404 below reads
# "/v1.41/..." because fake_client() always negotiates api_version 1.41; no
# assertion matches the number either way, only qr/is not supported/. So the
# daemon is faked below the socket instead, and the real _request runs. The
# success payload is the Engine API reference's own example descriptor.
#
# The one exception is the karr k38 subtest near the end, which drives the
# same ->exists 404-handling through Mock's route table instead of the real
t/lib/Test/API/Docker/Mock.pm view on Meta::CPAN
my $matched = 0;
if (exists $routes{$key}) {
$handler = $routes{$key};
$matched = 1;
}
else {
for my $pattern (keys %routes) {
my ($route_method, $route_path) = split /\s+/, $pattern, 2;
next unless $method eq $route_method;
# \Q...\E: $route_path is a literal path, not a regex a test author
# wrote on purpose -- interpolated unescaped, a route key containing
# a regex metacharacter (`.`, `?`, `+`, `(` ...) was read as a
# pattern rather than as the literal string it looks like, and could
# match a different path than the one it was registered for (or
# croak on an unbalanced `(`). This tier still tolerates only
# whitespace between method and path -- the exact-match branch above
# is the fast path for everything else. See t/mock_harness.t.
next unless $clean_path =~ m{^\Q$route_path\E$};
$handler = $routes{$pattern};
$matched = 1;
last;
t/mock_harness.t view on Meta::CPAN
# _mock_docker), so a route with a metacharacter has to be probed with a
# path that is close but not identical to reach it -- an exact hit never
# touches the regex at all.
check_live_access();
subtest 'a route key with a regex metacharacter is matched literally, not as a pattern' => sub {
plan skip_all => 'the mock route table is bypassed in live mode' if is_live();
# sha256:deadbeef.dead and sha256:deadbeefXdead differ only at the dot's
# position and are otherwise the same length. Unescaped,
# m{^sha256:deadbeef.dead$} reads the dot as "any character" and matches
# the second string too, even though only the first was ever registered.
my $docker = test_docker(
'GET /images/sha256:deadbeef.dead/json' => { Id => 'the-dotted-one' },
);
my $exact = $docker->images->inspect('sha256:deadbeef.dead');
is $exact->id, 'the-dotted-one',
'the literal path the route key names still matches -- this one is an '
. 'exact hash hit and never reaches the regex fallback at all';
my $err = do {
local $@;
eval { $docker->images->inspect('sha256:deadbeefXdead') };
$@;
};
like $err, qr/No mock route for/,
'a path that only accidentally resembles the route key -- same length, '
. 'differing at exactly the position the "." sat -- is refused rather '
. 'than matched. On the old, unescaped fallback this returned '
. 'the-dotted-one instead of croaking: a route for one image answering '
. 'for a different one';
};
done_testing;
t/plugins.t view on Meta::CPAN
# daemon constant -- re-measured live on 5.8.4 / API 1.44 the same message
# reads "/v1.44/plugins", karr k62) and every other path in the family
# answers a bare text/plain 404, i.e. the compat layer has no route for them
# at all. A live run of this file would therefore be red, and a skip_all
# would leave the whole class untested on the machine that actually runs the
# suite.
#
# So the daemon is faked below the socket instead, in both modes. Most of
# what is worth pinning about this endpoint family is in the request rather
# than the response -- a privilege list in a POST body, a query parameter
# that must not be omitted, a path that must not be escaped -- and that is
# what these assertions read.
#
# The canned responses are the Engine API reference's own example payloads,
# not daemon captures, which is why they are inline rather than in
# t/fixtures: a hand-rolled file there would claim a provenance it does not
# have.
sub fake_client {
my ($body, $status) = @_;
return Test::API::Docker::FakeTransport->new(
t/plugins.t view on Meta::CPAN
'and it goes back to the engine as a JSON boolean, so the round trip '
. 'still says what the daemon said';
# Without this the entity is inert: every method below reaches the engine
# through the client, and a wrapper built without one dies on an
# undefined invocant at the first call.
is $plugin->client, $c, 'the client is threaded into the entity';
};
# ---------------------------------------------------------------------------
subtest 'the plugin name reaches the path unescaped' => sub {
my $c = fake_client('{"Name":"docker.io/vieux/sshfs:latest"}');
$c->plugins->inspect('docker.io/vieux/sshfs:latest');
is request_line($c->written),
'GET /v1.41/plugins/docker.io/vieux/sshfs:latest/json HTTP/1.1',
'registry host, repository slashes and the tag colon all survive raw';
# The daemon routes this family as /plugins/{name:.*}/json, so the slashes
# are part of the captured name and percent-encoding them would not match.
unlike $c->written, qr/%2F|%3A/i, 'nothing in the name got percent-encoded';
t/plugins.t view on Meta::CPAN
is query_param($c->written, 'force'), '1', 'force => 1';
};
# ---------------------------------------------------------------------------
subtest 'privileges: remote in the query, list in the response' => sub {
my $c = fake_client(JSON::MaybeXS->new->encode($PRIVILEGES));
my $got = $c->plugins->privileges('vieux/sshfs');
is request_line($c->written),
'GET /v1.41/plugins/privileges?remote=vieux/sshfs HTTP/1.1',
'remote is a query parameter, and its slash is not escaped';
is_deeply $got, $PRIVILEGES, 'the privilege list comes back as an ArrayRef';
unlike $c->written, qr/X-Registry-Auth/i,
'no auth header without auth: the plugin router discards an '
. 'undecodable one, so anonymous needs none';
};
subtest 'privileges: a plugin that demands nothing answers null' => sub {
# computePrivileges starts from `var privileges types.PluginPrivileges` and
# appends only what the config asks for, so a plugin needing nothing sends
# a nil Go slice, which marshals to a bare `null`.
t/role_http.t view on Meta::CPAN
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/role_http.t view on Meta::CPAN
api_version => '1.41',
);
subtest 'CRLF in the name croaks and sends nothing' => sub {
eval {
$t->_request('POST', '/images/x/push',
headers => { "X-Registry-Auth\r\nX-Injected" => 'evil' });
};
like $@, qr/invalid header name/, 'croaked';
like $@, qr/\QX-Registry-Auth\x0D\x0AX-Injected\E/,
'the offending name is shown with its control bytes escaped, so the '
. 'message stays on one line and names what was actually passed';
my $message = "$@";
unlike $message, qr/\r/, 'the croak itself carries no raw CR';
$message =~ s/\n\z//;
unlike $message, qr/\n/,
'and no LF beyond the one Carp ends on -- the escaped name cannot open '
. 'a line of its own in whatever logs the failure';
is $t->_sink, undef,
'no socket was even opened -- the name is checked while the request is '
. 'assembled, so nothing reached the daemon';
};
subtest 'the separators that would corrupt the line, injection or not' => sub {
my %bad = (
'an embedded space' => 'X Registry Auth',
'a trailing colon' => 'X-Registry-Auth:',
t/role_http.t view on Meta::CPAN
my $t = Test::API::Docker::FakeTransport->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
subtest 'the measured injection: CRLF in the path croaks and sends nothing' => sub {
my $evil = "x HTTP/1.1\r\nX-Evil: 1\r\n\r\nGET /y";
eval { $t->_request('GET', "/containers/$evil/json") };
like $@, qr/invalid request path/, 'croaked';
like $@, qr/\Qx HTTP\E.*\Q\x0D\x0AX-Evil\E/,
'the offending path is shown with its control bytes escaped, so the '
. 'message stays on one line and names what was actually passed';
my $message = "$@";
unlike $message, qr/\r/, 'the croak itself carries no raw CR';
$message =~ s/\n\z//;
unlike $message, qr/\n/,
'and no LF beyond the one Carp ends on -- the escaped path cannot open a '
. 'line of its own in whatever logs the failure';
is $t->_sink, undef,
'no socket was even opened -- the path is checked while the request is '
. 'assembled, so nothing reached the daemon';
};
subtest 'each separator that would rewrite the request target' => sub {
my %bad = (
'a space (opens the HTTP-version field)' => 'na me',
'a bare CR' => "tail\r",
t/streaming_methods.t view on Meta::CPAN
"$name returned the summary and stopped where the callback said";
}
};
subtest 'the entity classes forward the callback too' => sub {
plan skip_all => 'live mode ignores the route table' if is_live();
# containers_list (karr k101 follow-up): a real capture -- see t/containers.t.
# The logs route names the fixture's own container id exactly, rather than
# a [^/]+ wildcard: Test::API::Docker::Mock's fallback tier used to
# interpolate a route key as an unescaped regex, which is what made a
# wildcard like that "work" here by accident (karr k109, t/mock_harness.t)
# -- now that it is quotemeta'd, a route key means the literal path it
# looks like.
my $docker = test_docker(
'GET /containers/json' => load_fixture('containers_list'),
'GET /containers/b20ac7508d80182ba3cd1cbd006ac10c8a15f4f7590fa89c2078d146caf96555/logs' => [
{ stream => 'stdout', data => "one\n" },
{ stream => 'stdout', data => "two\n" },
],
);
t/transport_shape.t view on Meta::CPAN
};
subtest 'element order is the caller\'s, key order is sorted' => sub {
my $c = fake_client('');
$c->get('/x', params => { z => 1, names => ['b', 'a'], a => 2 });
is $c->request_line, 'GET /v1.41/x?a=2&names=b&names=a&z=1 HTTP/1.1',
'keys sorted, but b still precedes a inside the list';
};
subtest 'the elements are escaped the way a single value is' => sub {
my $c = fake_client('');
$c->get('/images/get', params => { names => ['my repo/img:1', 'a&b=c'] });
is $c->request_line,
'GET /v1.41/images/get?names=my%20repo/img:1&names=a%26b%3Dc HTTP/1.1',
'a space, an ampersand and an equals sign are encoded; / and : are not';
};
subtest 'the empty and undef corners of a list' => sub {
my $c = fake_client('');