API-Docker
view release on metacpan or search on metacpan
t/system_auth.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use FindBin;
use lib "$FindBin::Bin/lib";
use JSON::MaybeXS qw( decode_json );
use API::Docker;
use Test::API::Docker::FakeTransport;
# POST /auth -- checking a set of registry credentials without pulling or
# pushing anything (karr k17).
#
# Nothing here performs a login, in either mode. No credential in this file
# is real and no request leaves the process: the daemon is faked below the
# socket so _request assembles a real request line, headers and body with
# nothing on the other end, and the assertions read what would have gone on
# the wire. That is the whole of what this method decides -- the outcome is
# the registry's to give.
#
# The canned success body is the Engine API reference's own example payload,
# and the canned failure bodies are captures from the rootless Podman socket
# (5.4.2, API 1.41), taken with an unreachable serveraddress so that no
# registry was ever asked about anyone's credentials:
#
# POST /v1.41/auth {"username":"nobody","password":"nothing",
# "serveraddress":"127.0.0.1:1"}
# -> 500 {"message":"login attempt to 127.0.0.1:1 failed with status:
# authenticating creds for \"127.0.0.1:1\": pinging container
# registry 127.0.0.1:1: Get \"https://127.0.0.1:1/v2/\": dial tcp
# 127.0.0.1:1: connect: connection refused"}
#
# Note the 500: Podman does not answer a failed check with Docker's 401. The
# croak is what both engines have in common, which is why this file asserts
# the croak and not the number.
# The socket is built lazily by the first request, so a call that croaked
# before sending has no sink at all -- which is itself the assertion in
# several subtests below. Test::API::Docker::FakeTransport's ->written
# guards for exactly that.
my $SUCCESS = '{"Status":"Login Succeeded","IdentityToken":""}';
sub fake_client {
my ($body, $status) = @_;
return Test::API::Docker::FakeTransport->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
canned => [$status // 200, 'OK', {}, $body // $SUCCESS],
);
}
sub request_line {
my ($raw) = @_;
my ($line) = $raw =~ /\A([^\r\n]*)/;
return $line;
}
sub request_body {
my ($raw) = @_;
my ($body) = $raw =~ /\r\n\r\n(.*)\z/s;
return $body;
}
my $CREDS = {
username => 'me',
password => 'secret',
serveraddress => 'ghcr.io',
};
# ---------------------------------------------------------------------------
subtest 'the credentials go in the body, not in a header' => sub {
my $c = fake_client();
my $out = $c->system->auth(%$CREDS);
is request_line($c->written), 'POST /v1.41/auth HTTP/1.1',
'POST /auth, no query string';
is_deeply decode_json(request_body($c->written)), $CREDS,
'the AuthConfig is the JSON request body';
# The same object push carries in X-Registry-Auth, but /auth is the one
# endpoint that takes it unencoded -- a header here would be ignored.
unlike $c->written, qr/X-Registry-Auth/i,
'no X-Registry-Auth header on this endpoint';
like $c->written, qr{Content-Type: application/json\r\n},
'sent as JSON';
is_deeply $out, { Status => 'Login Succeeded', IdentityToken => '' },
'the decoded engine answer is returned unwrapped';
};
# ---------------------------------------------------------------------------
subtest 'the auth argument push takes is accepted here too' => sub {
# The point of the second spelling: a caller can hand over exactly what it
# was about to push with, without taking it apart first.
my $c = fake_client();
$c->system->auth(auth => $CREDS);
is_deeply decode_json(request_body($c->written)), $CREDS,
'a HashRef reaches the body as itself';
my $encoded = $c->system->_registry_auth_header($CREDS);
my $b = fake_client();
$b->system->auth(auth => $encoded);
is_deeply decode_json(request_body($b->written)), $CREDS,
'and an already-encoded X-Registry-Auth value is decoded back into it';
my $j = fake_client();
$j->system->auth(auth => '{"identitytoken":"tok-123"}');
is_deeply decode_json(request_body($j->written)), { identitytoken => 'tok-123' },
'as is a raw JSON AuthConfig';
};
# ---------------------------------------------------------------------------
subtest 'the two spellings may not be mixed' => sub {
# Not a style rule: picking one silently would be picking which of two sets
# of credentials gets sent.
my $c = fake_client();
ok !eval { $c->system->auth(auth => $CREDS, username => 'other'); 1 },
'auth => $config beside a credential key croaks';
like $@, qr/not both/, 'and says which keys were seen';
is $c->written, '', 'nothing was sent';
};
# ---------------------------------------------------------------------------
subtest 'a check with nothing to check croaks before the request' => sub {
my $c = fake_client();
ok !eval { $c->system->auth; 1 }, 'no arguments croaks';
like $@, qr/requires credentials/, 'and says what is missing';
is $c->written, '', 'nothing was sent';
my $e = fake_client();
ok !eval { $e->system->auth(auth => {}); 1 },
'an empty AuthConfig croaks too';
is $e->written, '', 'nothing was sent';
# Stricter than the engine on purpose: Podman answers the empty body with
# 500 'getting username and password: cannot prompt for username without
# stdin', which describes the daemon's stdin rather than the caller's bug.
};
# ---------------------------------------------------------------------------
subtest 'only identitytoken is enough' => sub {
my $c = fake_client();
$c->system->auth(identitytoken => 'tok-123', serveraddress => 'ghcr.io');
is_deeply decode_json(request_body($c->written)),
{ identitytoken => 'tok-123', serveraddress => 'ghcr.io' },
'a token-only AuthConfig is a complete one';
};
# ---------------------------------------------------------------------------
subtest 'a rejected credential croaks, and the status is still readable' => sub {
# Docker's answer: 401 with {"message":...}.
my $c = fake_client('{"message":"Get \"https://ghcr.io/v2/\": unauthorized"}', 401);
my %res;
ok !eval { $c->system->auth(%$CREDS, response => \%res); 1 },
'a 401 croaks rather than returning a false value';
like $@, qr/unauthorized/, 'the registry message survives into the croak';
is $res{status}, 401,
'response is filled before the croak, so an eval-ing caller can tell '
. 'a rejected credential from an unreachable registry';
# Podman's answer to the same failure: 500, measured -- see the header
# comment. A caller testing for 401 would call this a success.
my $p = fake_client(
'{"message":"login attempt to ghcr.io failed with status: '
. 'authenticating creds for \"ghcr.io\": pinging container registry"}', 500);
my %pres;
ok !eval { $p->system->auth(%$CREDS, response => \%pres); 1 },
'Podman 500 croaks the same way';
is $pres{status}, 500, 'and reports its own status';
};
done_testing;
( run in 0.679 second using v1.01-cache-2.11-cpan-85d3896f969 )