API-Docker
view release on metacpan or search on metacpan
t/plugins.t view on Meta::CPAN
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';
};
subtest 'disable: force is optional' => sub {
my $c = fake_client('');
$c->plugins->disable('vieux/sshfs:latest');
is request_line($c->written),
'POST /v1.41/plugins/vieux/sshfs:latest/disable HTTP/1.1',
'no force parameter by default';
$c->plugins->disable('vieux/sshfs:latest', force => 1);
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`.
my $c = fake_client('null');
# karr k30 (fixed): the transport used to decode a body only when it
# started with { or [, so a bare JSON scalar came back as its own bytes --
# the four-character string 'null'. It now decodes any JSON body, scalars
# included, so this comes back as undef, same as decode_json('null') would.
is $c->get('/plugins/privileges', params => { remote => 'x' }), undef,
'the transport decodes the bare null to undef (karr k30)';
# Unguarded, that undef would be POSTed to /plugins/pull as a JSON null
# where the engine expects an array.
is_deeply $c->plugins->privileges('vieux/sshfs'), [],
'privileges normalises it to the empty list it means';
};
subtest 'privileges: auth is sent as padded base64url X-Registry-Auth' => sub {
my $c = fake_client('[]');
$c->plugins->privileges('private.example.com/p/sshfs',
auth => { username => 'me', password => 'secret' });
my ($hdr) = $c->written =~ /^X-Registry-Auth: (\S+)\r$/m;
ok defined $hdr, 'header present when auth was given';
is length($hdr) % 4, 0, 'padded, as Go base64.URLEncoding requires';
is_deeply decode_json(b64url_decode($hdr)),
{ username => 'me', password => 'secret' },
'header decodes to the credentials passed';
};
# ---------------------------------------------------------------------------
subtest 'install: the privilege list is the request body' => sub {
my $c = fake_client(qq({"status":"Pulling plugin"}\n));
my $events = $c->plugins->install('vieux/sshfs:latest',
privileges => $PRIVILEGES);
like request_line($c->written), qr{\APOST /v1\.41/plugins/pull\?},
'install is POST /plugins/pull';
is query_param($c->written, 'remote'), 'vieux/sshfs:latest',
'remote is a query parameter';
like $c->written, qr{^Content-Type: application/json\r$}m,
'the body is JSON, not a tarball';
is_deeply decode_json(request_body($c->written)), $PRIVILEGES,
'the privileges go back to the engine verbatim -- it compares them '
. 'against what the plugin demands and refuses a mismatch';
is_deeply $events, [ { status => 'Pulling plugin' } ],
'the NDJSON progress stream comes back as an ArrayRef of events';
};
subtest 'install: local name and auth' => sub {
my $c = fake_client(qq({"status":"Pulling plugin"}\n));
$c->plugins->install('vieux/sshfs:latest',
privileges => [],
name => 'sshfs',
auth => { identitytoken => 'tok-123' },
);
is query_param($c->written, 'name'), 'sshfs', 'local name sent';
is request_body($c->written), '[]',
'an empty privilege list is sent as an empty JSON array, not omitted';
my ($hdr) = $c->written =~ /^X-Registry-Auth: (\S+)\r$/m;
is_deeply decode_json(b64url_decode($hdr)), { identitytoken => 'tok-123' },
'identitytoken auth reaches the header';
};
subtest 'install: a failure inside the 200 stream croaks' => sub {
my $c = fake_client(
qq({"status":"Pulling plugin"}\n)
. qq({"errorDetail":{"message":"incorrect privileges"},"error":"incorrect privileges"}\n)
);
eval {
$c->plugins->install('vieux/sshfs:latest', privileges => []);
1;
};
my $err = $@;
ok $err, 'a failed install does not return quietly';
like "$err", qr/incorrect privileges/,
'the engine reason survives into the exception';
isa_ok $err, 'API::Docker::Error::Stream';
is_deeply [ map { $_->{status} } grep { $_->{status} } @{ $err->events } ],
['Pulling plugin'], 'the progress that preceded the failure is kept';
};
subtest 'install: accept_privileges resolves both calls' => sub {
my $docker = API::Docker->new(
host => 'unix:///nonexistent.sock',
t/plugins.t view on Meta::CPAN
'the same privilege body as install';
};
subtest 'upgrade: a locally renamed plugin upgrades from its remote' => sub {
my $c = fake_client(qq({"status":"Upgrading"}\n));
$c->plugins->upgrade('sshfs',
remote => 'vieux/sshfs:v2',
privileges => [],
);
like request_line($c->written), qr{\APOST /v1\.41/plugins/sshfs/upgrade\?},
'the local name is in the path';
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 => []) };
( run in 0.762 second using v1.01-cache-2.11-cpan-85d3896f969 )