API-Docker

 view release on metacpan or  search on metacpan

t/images_registry_auth.t  view on Meta::CPAN

# both go through the one encoder in API::Docker::Role::RegistryAuth. This file
# proves the two headers reach the wire correctly encoded, and pins the
# base64url-alphabet fix: a value pre-encoded in *standard* base64 must be
# respelled into the URL-safe alphabet before it goes out, because the engine
# decodes with Go's base64.URLEncoding and a leftover '+' or '/' makes it fail
# with 'failed to parse ... header ... unexpected EOF'.

# No padding is added back, and the standard alphabet is deliberately NOT mapped
# to URL-safe here -- decode exactly what the engine would receive, so an
# unconverted '+'/'/' on the wire is a decode failure the test can see.
sub b64url_decode {
  my ($s) = @_;
  $s =~ tr{-_}{+/};
  return decode_base64($s);
}

my $CREDS = { username => 'me', password => 'secret', serveraddress => 'ghcr.io' };

# ---------------------------------------------------------------------------
# The :63 fix, at the encoder. A pre-encoded standard-base64 string (with '+'
# and '/') used to pass through untouched; it must now be respelled URL-safe.
# This subtest goes red the moment the tr{+/}{-_} is dropped and the old
# `return $auth if ... base64-like` is restored.
subtest 'a pre-encoded standard-base64 auth is respelled URL-safe' => sub {
  my $images = API::Docker->new(
    host        => 'unix:///dev/null',
    api_version => '1.47',
  )->images;

  # Bytes 0..255 guarantee the standard alphabet's '+' and '/' appear.
  my $raw = pack('C*', 0 .. 255);
  my $std = encode_base64($raw, '');
  like $std, qr{[+/]}, 'the sample payload really contains + or /';

  my $out = $images->_registry_auth_header($std);
  unlike $out, qr{[+/]},
    'no standard-base64 character reaches the wire';

  (my $expected = $std) =~ tr{+/}{-_};
  is $out, $expected,
    'the header is exactly the URL-safe respelling of the input';
  is b64url_decode($out), $raw,
    'and it still decodes back to the original bytes';

  # X-Registry-Config's encoder shares the same body and the same fix.
  is $images->_registry_config_header($std), $expected,
    'the config-header encoder respells a standard-base64 string too';
};

# ---------------------------------------------------------------------------
subtest 'pull sends X-Registry-Auth only when auth is given' => sub {
  my $captured;
  my $docker = test_docker(
    'POST /images/create' => sub {
      my ($method, $path, %opts) = @_;
      $captured = \%opts;
      return '';
    },
  );

  # With credentials: header present, correctly encoded, no +// on the wire.
  $docker->images->pull(fromImage => 'private.example/app', tag => 'v1', auth => $CREDS);
  my $hdr = $captured->{headers}{'X-Registry-Auth'};
  ok defined $hdr, 'X-Registry-Auth present when auth given';
  unlike $hdr, qr{[+/]}, 'header uses the URL-safe alphabet only';
  is_deeply decode_json(b64url_decode($hdr)), $CREDS,
    'header decodes to the passed credentials';

  # Without credentials: no header at all -- the anonymous case.
  $captured = undef;
  $docker->images->pull(fromImage => 'alpine', tag => '3');
  ok !exists $captured->{headers}, 'no X-Registry-Auth header on an anonymous pull';
};

# ---------------------------------------------------------------------------
subtest 'build sends X-Registry-Config only when registry_config is given' => sub {
  my $captured;
  my $docker = test_docker(
    'POST /build' => sub {
      my ($method, $path, %opts) = @_;
      $captured = \%opts;
      return [ { stream => 'Successfully built deadbeef\n' } ];
    },
  );

  my $map = {
    'registry.example:5000' => { username => 'me', password => 'secret' },
    'ghcr.io'               => { identitytoken => 'tok-123' },
  };

  $docker->images->build(
    context         => 'fake-tar-data',
    t               => 'myapp:latest',
    registry_config => $map,
  );
  my $hdr = $captured->{headers}{'X-Registry-Config'};
  ok defined $hdr, 'X-Registry-Config present when registry_config given';
  unlike $hdr, qr{[+/]}, 'header uses the URL-safe alphabet only';
  is_deeply decode_json(b64url_decode($hdr)), $map,
    'header decodes to the hostname -> AuthConfig map';
  ok !exists $captured->{headers}{'X-Registry-Auth'},
    'a build carries the map header, not the single-AuthConfig one';

  # Without it: no header at all.
  $captured = undef;
  $docker->images->build(context => 'fake-tar-data', t => 'myapp:latest');
  ok !exists $captured->{headers}, 'no X-Registry-Config header on an anonymous build';
};

done_testing;



( run in 0.810 second using v1.01-cache-2.11-cpan-85d3896f969 )