API-Docker

 view release on metacpan or  search on metacpan

t/stream_error.t  view on Meta::CPAN

  is $@, '', 'and neither does one that literally carries errorDetail';
  is scalar @$events, 2, 'the event is handed over as data';
};

# ---------------------------------------------------------------------------
subtest 'the stringification contract' => sub {
  my $t = transport(load_fixture_raw('images_build_error_stream.ndjson'));
  eval { $t->images->build(context => 'tar-bytes') };
  my $err = $@;

  # Everything else in this distribution croaks strings and consumers rely on
  # it -- ../p5-dist-zilla-plugin-docker-api strips Carp's location tail off
  # $@ with a substitution. All of this has to keep working unchanged.
  like "$err", qr/ at \S+ line \d+\.?/,
    'carries Carp\'s location suffix, exactly as the plain croak it replaces';
  like $err, qr/exit status 7/, 'matches a regex without an explicit stringify';
  ok $err, 'boolean-true, so if ($@) still detects it';
  is $err . '', "$err", 'concatenation goes through the overload';
  is sprintf('%s', $err), "$err", 'so does sprintf %s';
  ok $err eq "$err", 'and string comparison';

  is $err->message . $err->location, "$err",
    'message and location are separable, and together they are the string';
  unlike $err->message, qr/ at \S+ line \d+/,
    'message on its own is the bare reason, no location';

  # The consumer's exact treatment, from Dist::Zilla::Plugin::Docker::API.
  my $copy = $err;
  $copy =~ s/\s+at\s+\S+\s+line\s+\d+\.?//g;
  $copy =~ s/\s+/ /g;
  $copy =~ s/^\s+|\s+$//g;
  is ref \$copy, 'SCALAR', 's/// on the exception yields a plain string';
  like $copy, qr/exit status 7\z/,
    'the location is stripped and the reason survives';
  isa_ok $err, 'API::Docker::Error::Stream',
    'the original is untouched by the copy\'s substitution';
};

subtest 'a message that ends in a newline still gets a location' => sub {
  # Engine messages end in "\n" and Carp appends no location to a message
  # that already does, so the trailing whitespace has to come off first.
  my $body = encode_json({ errorDetail => { message => "boom\n" } }) . "\n";
  eval { transport($body)->_request('POST', '/build', ndjson => 1) };
  like "$@", qr/boom at \S+ line \d+\./,
    'the trailing newline is trimmed and the suffix lands';
};

subtest 'errorDetail without a usable message falls back' => sub {
  my $body = encode_json({ errorDetail => {}, error => 'flat error text' }) . "\n";
  eval { transport($body)->_request('POST', '/build', ndjson => 1) };
  like "$@", qr/flat error text/,
    'the flat error key is the fallback when errorDetail has no message';

  $body = encode_json({ errorDetail => {} }) . "\n";
  eval { transport($body)->_request('POST', '/build', ndjson => 1) };
  like "$@", qr/no message given/, 'and there is a last resort';
  isa_ok $@, 'API::Docker::Error::Stream', 'still the exception class';
};

subtest 'the query string stays out of the message' => sub {
  # /build carries buildargs in the query string, which can hold credentials.
  my $body = encode_json({ errorDetail => { message => 'nope' } }) . "\n";
  eval {
    transport($body)->_request('POST', '/build',
      ndjson => 1,
      params => { t => 'app:v1', buildargs => { NPM_TOKEN => 'sekrit' } },
    );
  };
  like "$@", qr{\QPOST /v1.41/build\E}, 'the endpoint is named';
  unlike "$@", qr/sekrit/, 'the build args are not';
};

# ---------------------------------------------------------------------------
SKIP: {
  skip 'mock routes are bypassed in live mode', 1 if is_live();

  subtest 'which endpoints opt out of the check' => sub {
    my %saw;
    my $docker = test_docker(
      'POST /build'             => sub { $saw{build} = { @_[2 .. $#_] }; [] },
      'POST /images/create'     => sub { $saw{pull}  = { @_[2 .. $#_] }; [] },
      'POST /images/nginx/push' => sub { $saw{push}  = { @_[2 .. $#_] }; [] },
      'GET /events'             => sub { $saw{events} = { @_[2 .. $#_] }; [] },
    );

    $docker->images->build(context => 'tar-bytes', t => 'x:1');
    $docker->images->pull(fromImage => 'nginx');
    $docker->images->push('nginx');
    $docker->system->events(since => 1, until => 2);

    # The check defaults on, so an operation endpoint says nothing at all --
    # a new streaming endpoint gets the loud behaviour without being told.
    ok !exists $saw{$_}{croak_on_error}, "$_ leaves the check at its default"
      for qw( build pull push );
    is $saw{events}{croak_on_error}, 0, 'events is the one endpoint opting out';
  };
}

SKIP: {
  skip 'live write tests disabled (API_DOCKER_TEST_WRITE=1 to enable)', 1
    unless can_write();

  subtest 'live: a build that fails croaks against a real engine' => sub {
    # Measured, not assumed: the engine answers 200 and reports the failure
    # inside the stream, and the build never commits an image. But rm=1 (the
    # default) only removes the intermediate container on a *successful*
    # build -- a failing RUN step leaves it behind, under an engine-chosen
    # name (Docker: priceless_driscoll, ...) or, on Podman, as a buildah
    # "working container" that GET /containers/json never lists at all
    # (measured against 5.8.4). karr k63: a day of live runs stranded 14 such
    # containers, on both engines, invisible to register_cleanup because none
    # of them are named by the test -- the daemon creates them, not us.
    #
    # forcerm=1 is the engine's own answer to exactly this case ("always
    # remove intermediate containers, even upon failure"). The build stream
    # was checked first, as the ticket suggested: Docker's classic builder
    # does put the id in a "Running in <id>" stream line, but Podman's
    # buildah backend never mentions an id anywhere in the stream or the
    # response headers, so scraping the stream cannot be the general fix.
    # forcerm=1 was then measured directly (raw curl probes against both
    # sockets, karr k63) to close the leak on both: Docker's stream gains a



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