API-Docker
view release on metacpan or search on metacpan
t/containers.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use lib 't/lib';
use Test::API::Docker::Mock;
use API::Docker::Role::Entity::Container;
use JSON::MaybeXS qw( encode_json );
check_live_access();
# --- Read Tests (always run) ---
# containers_list/container_inspect (karr k101 follow-up): a real capture,
# taken 2026-08-29 against Docker 29.7.2 (API 1.55) from a disposable
# apidocker-fixture-probe-<random> container (alpine:3, running `sleep 300`)
# created for the purpose and removed again once both endpoints were
# captured -- see the header of t/type_fixture_passthrough.t. Both fixtures
# describe the same container, so their Id fields agree; the list endpoint
# was filtered to that one container so the fixture stays small and
# deterministic, which is why there is only one to assert on here (a second,
# non-running container's is_running shape is covered directly, without a
# fixture, in t/entity_container.t's "is_running reads whichever of the two
# shapes it is on").
subtest 'list containers' => sub {
my $docker = test_docker(
'GET /containers/json' => load_fixture('containers_list'),
);
my $containers = $docker->containers->list(all => 1);
is(ref $containers, 'ARRAY', 'returns array');
if (@$containers) {
isa_ok($containers->[0], 'API::Docker::Type::ContainerSummary');
ok($containers->[0]->id, 'has an id');
}
unless (is_live()) {
is(scalar @$containers, 1, 'one container');
my $first = $containers->[0];
is($first->id,
'b20ac7508d80182ba3cd1cbd006ac10c8a15f4f7590fa89c2078d146caf96555',
'container id');
is_deeply($first->names, ['/apidocker-fixture-probe-39a25e34'],
'container names');
is($first->image, 'alpine:3', 'container image');
is($first->state, 'running', 'container state');
ok($first->is_running, 'is_running returns true for running container');
}
};
# --- Write Tests (mock always, live only with WRITE) ---
subtest 'container lifecycle' => sub {
skip_unless_write();
my $name = 'api-docker-test-' . $$;
my $docker = test_docker(
'POST /containers/create' => sub {
my ($method, $path, %opts) = @_;
# containers->create moves `name` out of the config and into the
# query string (Containers.pm) -- the rest of the config is the JSON
# body. Assert both, so a regression that leaked `name` into the body
# instead (or dropped it from the query) would fail here rather than
# only being visible by reading the code.
is($opts{params}{name}, $name, 'the container name reached the query string')
unless is_live();
ok(!exists $opts{body}{name}, 'and never the JSON body') unless is_live();
is_deeply($opts{body}, { Image => 'alpine:3', Cmd => ['sleep', '10'] },
'the rest of the config is the body, name excepted') unless is_live();
return { Id => 'mock123', Warnings => [] };
},
'POST /containers/mock123/start' => undef,
'GET /containers/mock123/json' => load_fixture('container_inspect'),
'GET /containers/mock123/top' => {
Titles => ['UID', 'PID', 'PPID', 'C', 'STIME', 'TTY', 'TIME', 'CMD'],
Processes => [
['root', '12345', '1', '0', '08:00', '?', '00:00:00', 'sleep'],
],
},
'GET /containers/mock123/stats' => {
cpu_stats => { cpu_usage => { total_usage => 1000 } },
memory_stats => { usage => 50000000 },
},
'POST /containers/mock123/pause' => undef,
'POST /containers/mock123/unpause' => undef,
'POST /containers/mock123/stop' => undef,
'DELETE /containers/mock123' => undef,
);
if (is_live()) {
# containers->create does not auto-pull; alpine:3 (the tag the rest of
# the suite standardises on) is not guaranteed to be cached on a fresh
# checkout. Pull it explicitly rather than relying on another test
# file -- or an earlier manual run -- having already done so: prove's
# file order is not a contract this test should depend on.
$docker->images->pull(fromImage => 'alpine', tag => '3');
}
my $created = $docker->containers->create(
name => $name,
Image => 'alpine:3',
Cmd => ['sleep', '10'],
);
ok($created->{Id}, 'created container has Id');
my $id = is_live() ? $created->{Id} : 'mock123';
# Safety net for a die before the remove below; on the happy path the
# container is already gone, and that must not warn.
register_cleanup(sub {
eval { $docker->containers->remove($id, force => 1) };
t/containers.t view on Meta::CPAN
is($body->{Memory}, 314572800, 'the resource limit reached the body');
my $json = encode_json($body);
like($json, qr/"Init":true/, 'Init => 1 goes out as JSON true, k100\'s normalisation');
like($json, qr/"OomKillDisable":false/, 'OomKillDisable => 0 goes out as JSON false');
unlike($json, qr/"(?:Init|OomKillDisable)":[01]/,
'no bare 1/0 for either boolean field');
};
subtest 'kill sends its signal to the container\'s own endpoint' => sub {
plan skip_all => 'route assertions are fixture-only' if is_live();
my $params;
my $docker = test_docker(
'POST /containers/deadbeef/kill' => sub {
my ($method, $path, %opts) = @_;
$params = $opts{params};
return undef;
},
);
$docker->containers->kill('deadbeef', signal => 'SIGUSR1');
is_deeply($params, { signal => 'SIGUSR1' }, 'the signal reached the query string');
};
subtest 'rename posts the new name as a query param' => sub {
plan skip_all => 'route assertions are fixture-only' if is_live();
my $params;
my $docker = test_docker(
'POST /containers/deadbeef/rename' => sub {
my ($method, $path, %opts) = @_;
$params = $opts{params};
return undef;
},
);
$docker->containers->rename('deadbeef', 'new-name');
is_deeply($params, { name => 'new-name' },
'the new name reached the query string, not the body');
};
subtest 'prune sends its filters and returns the daemon response' => sub {
plan skip_all => 'route assertions are fixture-only' if is_live();
my $params;
my $docker = test_docker(
'POST /containers/prune' => sub {
my ($method, $path, %opts) = @_;
$params = $opts{params};
return { ContainersDeleted => ['abc'], SpaceReclaimed => 100 };
},
);
my $result = $docker->containers->prune(filters => { until => ['24h'] });
is_deeply($result, { ContainersDeleted => ['abc'], SpaceReclaimed => 100 },
'the daemon response is returned unwrapped');
is_deeply($params->{filters}, { until => ['24h'] },
'the filters reached the query string, shape-normalised');
};
# --- Validation Tests (always run, no Docker needed) ---
subtest 'container ID required' => sub {
my $docker = test_docker();
eval { $docker->containers->inspect(undef) };
like($@, qr/Container ID required/, 'croak on missing ID for inspect');
eval { $docker->containers->start(undef) };
like($@, qr/Container ID required/, 'croak on missing ID for start');
eval { $docker->containers->stop(undef) };
like($@, qr/Container ID required/, 'croak on missing ID for stop');
};
done_testing;
( run in 1.142 second using v1.01-cache-2.11-cpan-364913b4093 )