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 )