API-Docker

 view release on metacpan or  search on metacpan

t/images_tar.t  view on Meta::CPAN

#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use FindBin;
use lib "$FindBin::Bin/lib";
use Test::API::Docker::Mock;
use API::Docker;

# karr k20 -- the image tar roundtrip: GET /images/{name}/get,
# GET /images/get?names= and POST /images/load. The air-gapped path: export
# here, carry the tar over, load there.
#
# t/fixtures/images_get.tar is a real export from the rootless Podman socket
# (5.4.2, API 1.41) -- a one-layer image imported through
# /images/create?fromSrc=- for the purpose, exported, then deleted again.
# t/fixtures/images_load_stream.ndjson is the body loading it back produced.
# Both are byte-exact captures, which is the whole point of the ticket: the
# export is raw tar and nothing in the client may touch it.

check_live_access();

my $TAR    = load_fixture_raw('images_get.tar');
my $STREAM = load_fixture_raw('images_load_stream.ndjson');

# ---------------------------------------------------------------------------
# A client whose socket is a captured in-memory sink and whose response is
# canned, so the real _request -- and therefore `raw => 1` and the ndjson
# decode -- runs without a daemon. Mirrors the FakeTransport pattern in
# t/streaming_shape.t and t/role_http.t; the mock harness replaces _request
# wholesale and cannot reach any of it.
package Test::ImagesTar::FakeTransport;
use Moo;
extends 'API::Docker';

has canned => (is => 'rw', default => sub { [200, 'OK', {}, ''] });
has _sink  => (is => 'rw');

sub _build__socket {
  my ($self) = @_;
  my $sink = '';
  $self->_sink(\$sink);
  open my $fh, '>', \$sink or die "open: $!";
  binmode $fh;
  return $fh;
}

sub _read_response { return $_[0]->canned }

sub written { return ${ $_[0]->_sink } }

package main;

sub fake_client {
  return Test::ImagesTar::FakeTransport->new(
    host        => 'unix:///nonexistent.sock',
    api_version => '1.41',
  );
}

# ---------------------------------------------------------------------------
subtest 'the fixture really is a tar, so byte-exactness means something' => sub {
  cmp_ok length($TAR), '>', 512, 'more than one tar block';
  is length($TAR) % 512, 0, 'a whole number of 512-byte blocks';
  is substr($TAR, 257, 5), 'ustar', 'ustar magic in the first header block';
  like $TAR, qr/manifest\.json/, 'carries a manifest member';
  like $TAR, qr/\0/, 'carries NUL bytes -- it is not text';
};

# ---------------------------------------------------------------------------
subtest 'get: asks for the export path and does not decode the answer' => sub {
  plan skip_all => 'route assertions are fixture-only' if is_live();

  my %seen;
  my $docker = test_docker(
    'GET /images/alpine:3/get' => sub {
      my ($method, $path, %opts) = @_;
      %seen = %opts;
      return $TAR;
    },
  );

  my $out = $docker->images->get('alpine:3');

  ok $seen{raw}, 'the request asked the transport for raw bytes';
  ok !$seen{ndjson}, 'and not for a decoded event stream';
  is $out, $TAR, 'the daemon bytes come back verbatim';
  is length($out), length($TAR), 'no truncation';

  my $err = do { local $@; eval { $docker->images->get }; $@ };
  like $err, qr/Image name required/, 'a missing name croaks';
};

# ---------------------------------------------------------------------------
subtest 'get: raw bytes survive the real _request' => sub {
  my $t = fake_client();
  $t->canned([200, 'OK', { 'content-type' => 'application/octet' }, $TAR]);

  my $out = $t->images->get('alpine:3');

  is $out, $TAR, 'byte-exact through _request';
  like $t->written, qr{\AGET /v1\.41/images/alpine:3/get HTTP/1\.1\r\n},
    'the request line keeps the colon in the image reference';
};

subtest 'get: a body that looks like JSON is still not decoded' => sub {
  # The transport tries decode_json on any body starting with { or [ unless
  # raw is set. A tar cannot start that way, but the guarantee under test is
  # "never decoded", not "never decodable" -- so assert it directly.
  my $t = fake_client();
  $t->canned([200, 'OK', {}, '{"Id":"not really a tar"}']);

  is ref $t->images->get('alpine:3'), '',
    'a JSON-shaped body comes back as a plain string, not a HashRef';
};



( run in 0.711 second using v1.01-cache-2.11-cpan-364913b4093 )