API-Docker
view release on metacpan or search on metacpan
t/plugins.t view on Meta::CPAN
# familiar and tagged, PluginReference fully qualified. No daemon reachable
# from this repo serves /plugins, so they are not a capture.
my $PLUGIN_JSON = '{'
. '"Id":"5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078",'
. '"Name":"vieux/sshfs:latest",'
. '"Enabled":true,'
. '"PluginReference":"docker.io/vieux/sshfs:latest",'
. '"Settings":{"Mounts":[],"Env":["DEBUG=0"],"Args":[],"Devices":[]},'
. '"Config":{"Description":"sshFS plugin for Docker",'
. '"Interface":{"Types":["docker.volumedriver/1.0"],"Socket":"sshfs.sock"}}'
. '}';
# ---------------------------------------------------------------------------
subtest 'list: filters are JSON-encoded, and the filter name is "enabled"' => sub {
my $c = fake_client('[]');
$c->plugins->list(filters => { enabled => ['true'] });
like request_line($c->written), qr{\AGET /v1\.41/plugins\?}, 'GET /plugins';
is_deeply decode_json(query_param($c->written, 'filters')),
{ enabled => ['true'] },
'the filters HashRef reaches the wire as a JSON map of string to array '
. 'of string';
# Not "enable". The Engine API reference documents that spelling, but the
# daemon validates plugin filter names against {enabled, capability} and
# refuses an unknown one outright, so copying the reference is a hard
# error rather than a silently empty list.
like query_param($c->written, 'filters'), qr/"enabled"/,
'the name sent is enabled, not enable';
};
subtest 'list: no filters means no query string' => sub {
my $c = fake_client('[]');
my $got = $c->plugins->list;
is request_line($c->written), 'GET /v1.41/plugins HTTP/1.1',
'an empty params hash appends nothing';
is_deeply $got, [], 'an engine with no plugins wraps to an empty ArrayRef';
};
# ---------------------------------------------------------------------------
subtest 'list wraps into API::Docker::Type::Plugin entities' => sub {
my $c = fake_client('[' . $PLUGIN_JSON . ']');
my $plugins = $c->plugins->list;
is ref $plugins, 'ARRAY', 'an ArrayRef, one entry per plugin';
my $plugin = $plugins->[0];
isa_ok $plugin, 'API::Docker::Type::Plugin';
is $plugin->id,
'5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078',
'id';
is $plugin->name, 'vieux/sshfs:latest', 'name';
is $plugin->plugin_reference, 'docker.io/vieux/sshfs:latest',
'plugin_reference -- the remote the plugin came from, which is not the '
. 'local name once a plugin is installed under one';
# Settings and Config used to be handed through as the HashRefs the engine
# sent; the generated model inflates them into classes of their own, so
# what they carry is reached by accessor a level down rather than by key.
is_deeply $plugin->settings->env, ['DEBUG=0'],
'settings is an API::Docker::Type::Plugin::Settings, and its env is the '
. 'list of KEY=value strings configure() takes';
is $plugin->config->interface->socket, 'sshfs.sock',
'config likewise, two levels of generated class down';
# The canned payload above is the Engine API reference's example and carries
# no Config.Env, so the two shapes of that one key are asserted on the model
# rather than on a payload invented to show them.
my $settings_env = 'API::Docker::Type::Plugin::Settings'
->docker_attributes->{env}{isa}->display_name;
my $config_env = 'API::Docker::Type::Plugin::Config'
->docker_attributes->{env}{isa}->display_name;
like $settings_env, qr/Str/, 'Settings.Env is declared as strings';
like $config_env, qr/PluginEnv/,
'and Config.Env as PluginEnv objects -- same wire name, two shapes, one '
. 'level apart';
# This claim is the reverse of the one the hand-written entity carried.
# API::Docker::Plugin mirrored the daemon verbatim, so Enabled stayed the
# decoded JSON boolean and the test asserted is_bool on it. The generated
# class declares the field as Bool and normalises it on the way in, so the
# accessor is a plain 1/0 -- and TO_JSON turns it back into a JSON boolean,
# which is where "nothing is lost" now lives.
is $plugin->enabled, 1, 'enabled is 1 for an enabled plugin';
ok !is_bool($plugin->enabled),
'a plain Perl 1, not the decoded JSON boolean -- the model normalises a '
. 'declared Bool rather than passing it through';
ok is_bool($plugin->TO_JSON->{Enabled}),
'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';
};
subtest 'inspect wraps too' => sub {
my $c = fake_client($PLUGIN_JSON);
my $plugin = $c->plugins->inspect('vieux/sshfs:latest');
isa_ok $plugin, 'API::Docker::Type::Plugin';
is $plugin->name, 'vieux/sshfs:latest', 'the single object is wrapped, '
. 'not returned as the HashRef it arrived as';
ok $plugin->enabled, 'and its fields are reachable as accessors';
my $again = $plugin->inspect;
is request_line($c->written),
'GET /v1.41/plugins/vieux/sshfs:latest/json HTTP/1.1',
'the entity re-inspects itself by name';
isa_ok $again, 'API::Docker::Type::Plugin';
};
subtest 'the entity threads its name back through the resource class' => sub {
my $c = fake_client('[' . $PLUGIN_JSON . ']');
my $plugin = $c->plugins->list->[0];
# Each request writes to a fresh sink, so ->written is always the last one.
$plugin->enable(timeout => 30);
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/enable?timeout=30 HTTP/1.1',
'enable: the name goes into the path and the option is passed on';
$plugin->disable(force => 1);
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/disable?force=1 HTTP/1.1',
'disable';
$plugin->configure(['DEBUG=1']);
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/set HTTP/1.1', 'configure';
is_deeply decode_json(request_body($c->written)), ['DEBUG=1'],
'and the settings survive the extra hop as an array';
$plugin->upgrade(privileges => $PRIVILEGES);
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/upgrade?remote=vieux/sshfs:latest'
. ' HTTP/1.1',
'upgrade: remote defaults to the local name, which is why the entity '
. 'carries plugin_reference for the renamed case';
is_deeply decode_json(request_body($c->written)), $PRIVILEGES,
'the privilege body is not swallowed by the delegation';
# The socket is an in-memory sink; this reaches no registry, and there is
# no live variant of this call anywhere in the suite.
$plugin->push;
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/push HTTP/1.1',
'push, which shadows the builtin in both packages and is only ever '
. 'called as a method';
$plugin->remove(force => 1);
is request_line($c->written),
'DELETE /v1.41/plugins/vieux/sshfs:latest?force=1 HTTP/1.1', 'remove';
};
subtest 'remove: force is normalised to 1/0 and omitted when unset' => sub {
my $c = fake_client('');
$c->plugins->remove('vieux/sshfs:latest');
is request_line($c->written),
'DELETE /v1.41/plugins/vieux/sshfs:latest HTTP/1.1',
'no force parameter when the caller passed none';
$c->plugins->remove('vieux/sshfs:latest', force => 1);
is query_param($c->written, 'force'), '1', 'force => 1';
$c->plugins->remove('vieux/sshfs:latest', force => 0);
is query_param($c->written, 'force'), '0',
'an explicit false is still sent, as 0';
};
# ---------------------------------------------------------------------------
subtest 'enable: timeout is always sent' => sub {
my $c = fake_client('');
$c->plugins->enable('vieux/sshfs:latest');
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/enable?timeout=0 HTTP/1.1',
'timeout=0 is sent even though the caller passed no timeout';
# This is not tidiness. The daemon reads the raw query value and parses it
# with Go's strconv.Atoi, with no default: an absent timeout is parsed as
# the empty string and the request fails with
# `strconv.Atoi: parsing "": invalid syntax`. Making this parameter
# conditional, the way force and filters are, would break every enable.
$c->plugins->enable('vieux/sshfs:latest', timeout => 30);
is query_param($c->written, 'timeout'), '30', 'an explicit timeout is used';
$c->plugins->enable('vieux/sshfs:latest', timeout => 0);
is query_param($c->written, 'timeout'), '0', 'an explicit 0 stays 0';
};
t/plugins.t view on Meta::CPAN
is query_param($c->written, 'remote'), 'vieux/sshfs:v2',
'the remote reference is in the query';
};
subtest 'upgrade: accept_privileges asks about the remote, not the local name' => sub {
my $docker = API::Docker->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
my @calls;
no warnings 'redefine';
local *API::Docker::_request = sub {
my ($self, $method, $path, %opts) = @_;
push @calls, { path => $path, %opts };
return $PRIVILEGES if $path eq '/plugins/privileges';
return [];
};
$docker->plugins->upgrade('sshfs', remote => 'vieux/sshfs:v2',
accept_privileges => 1);
is $calls[0]{params}{remote}, 'vieux/sshfs:v2',
'an upgrade is where the demands can change, so the new reference is '
. 'what gets asked about';
};
subtest 'upgrade: privileges are required too' => sub {
my $docker = API::Docker->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
eval { $docker->plugins->upgrade('vieux/sshfs:latest') };
like $@, qr/upgrade requires privileges/, 'refused, naming the method';
};
# ---------------------------------------------------------------------------
subtest 'push: request assembly only' => sub {
# This never reaches a registry -- the socket is an in-memory sink. There
# is no live variant of this subtest and there must not be one.
my $c = fake_client(qq({"status":"Pushing"}\n));
$c->plugins->push('myrepo/sshfs:v1', auth => { username => 'u', password => 'p' });
is request_line($c->written),
'POST /v1.41/plugins/myrepo/sshfs:v1/push HTTP/1.1',
'POST /plugins/{name}/push, no query string';
my ($hdr) = $c->written =~ /^X-Registry-Auth: (\S+)\r$/m;
is_deeply decode_json(b64url_decode($hdr)), { username => 'u', password => 'p' },
'credentials in X-Registry-Auth, which the reference does not document '
. 'on this endpoint but the daemon reads';
};
subtest 'push: anonymous sends no auth header' => sub {
my $c = fake_client(qq({"status":"Pushing"}\n));
$c->plugins->push('myrepo/sshfs:v1');
unlike $c->written, qr/X-Registry-Auth/i,
'unlike images->push, which must always send one';
};
# ---------------------------------------------------------------------------
subtest 'configure: settings are a JSON array of strings' => sub {
my $c = fake_client('');
$c->plugins->configure('vieux/sshfs:latest', ['DEBUG=1', 'sshkey.source=/tmp']);
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/set HTTP/1.1',
'POST /plugins/{name}/set';
is_deeply decode_json(request_body($c->written)),
['DEBUG=1', 'sshkey.source=/tmp'], 'body is the settings array';
$c->plugins->configure('vieux/sshfs:latest', 'DEBUG=1');
is_deeply decode_json(request_body($c->written)), ['DEBUG=1'],
'a bare list is accepted and still sent as an array, so a single '
. 'setting cannot become a JSON string by accident';
};
subtest 'configure: validation' => sub {
my $docker = API::Docker->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
eval { $docker->plugins->configure('vieux/sshfs:latest') };
like $@, qr/requires at least one setting/, 'no settings is refused';
eval { $docker->plugins->configure('vieux/sshfs:latest', []) };
like $@, qr/requires at least one setting/, 'an empty ArrayRef too';
eval { $docker->plugins->configure('vieux/sshfs:latest', { DEBUG => 1 }) };
like $@, qr/settings must be plain strings/,
'a HashRef is refused rather than encoded as an object';
eval { $docker->plugins->configure() };
like $@, qr/plugin name required/, 'and the name is required';
};
# ---------------------------------------------------------------------------
subtest 'every name-taking method croaks without a name' => sub {
my $docker = API::Docker->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
for my $method (qw( inspect remove enable disable push )) {
eval { $docker->plugins->$method(undef) };
like $@, qr/plugin name required/, "$method croaks on an undefined name";
}
eval { $docker->plugins->upgrade(undef, privileges => []) };
like $@, qr/plugin name required/, 'upgrade croaks on an undefined name';
eval { $docker->plugins->privileges(undef) };
like $@, qr/remote reference required/,
'privileges croaks on an undefined remote';
};
done_testing;
( run in 0.356 second using v1.01-cache-2.11-cpan-aadc1410aed )