API-Docker

 view release on metacpan or  search on metacpan

t/containers_stats_error.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More;
use lib 't/lib';
use Test::API::Docker::Mock;
use API::Docker;
use API::Docker::Error::HTTP;

# Regression coverage for karr k45: GET /containers/{id}/stats for a container
# that is not running is answered by Podman with an error object inside a
# response already committed to 200 --
#
#   {"cause":"container is stopped","message":"container is stopped",
#    "response":500}
#
# -- which neither guard in the transport sees (the >= 400 croak reads the
# status line, the stream check triggers on errorDetail). The one-shot call
# returned that HashRef where a reading was expected and an on_event callback
# was handed it as though it were one. containers->stats now croaks with an
# API::Docker::Error::HTTP instead.
#
# The rule is endpoint-local and deliberately narrow; this file exists to keep
# it that way. The counter-examples below are the point of it as much as the
# positive cases are: POST /containers/{id}/wait answers its SUCCESS case with
# a top-level Error key, so a case-insensitive rule would turn every
# successful wait into a failure, and Podman's own GET /plugins 404 carries
# the same three keys with response => 0.
#
# Measured 2026-08-27 against rootless Podman 5.4.2 (API 1.41) and Docker
# 29.7.2 (API 1.55) on one machine. Nothing here opens a socket: the route
# table of Test::API::Docker::Mock stands in for the daemon, and the live
# subtest at the end runs only with API_DOCKER_TEST_WRITE=1.

check_live_access();

my $STOPPED = {
  cause    => 'container is stopped',
  message  => 'container is stopped',
  response => 500,
};

my $READING = {
  read         => '2026-08-27T00:00:00Z',
  cpu_stats    => { cpu_usage => { total_usage => 1000 } },
  memory_stats => { usage => 50_000_000 },
};

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

  subtest 'the one-shot call croaks instead of returning the error object' => sub {
    my $docker = test_docker('GET /containers/abc/stats' => $STOPPED);

    my $stats = eval { $docker->containers->stats('abc') };
    my $err = $@;

    ok !defined $stats, 'nothing is returned -- the failure is not a value';
    isa_ok $err, 'API::Docker::Error::HTTP';
    is $err->status, 500,
      'the status is the one Podman named in `response`, not the 200 it sent';
    is_deeply $err->data, $STOPPED,
      'the whole object is carried, so ->data->{cause} is still readable';
    like "$err", qr/container is stopped/, 'the engine\'s own reason';
    like "$err", qr{\Qinside a 200 response to GET /containers/abc/stats\E},
      'and the fact that makes it hard to find otherwise';
    like "$err", qr/ at \S+ line \d+\.?/,
      'with Carp\'s location suffix, as every other croak here has';
    ok $err, 'boolean-true, so if ($@) detects it';
  };

  subtest 'stream => 1 without a callback croaks on the same body' => sub {
    # The buffered ndjson path: the same body arrives as a one-element list.
    my $docker = test_docker('GET /containers/abc/stats' => [$STOPPED]);

    my $stats = eval { $docker->containers->stats('abc', stream => 1) };
    isa_ok $@, 'API::Docker::Error::HTTP';
    ok !defined $stats, 'no ArrayRef with an error object in it comes back';
  };

  subtest 'on_event: the callback never sees the error object' => sub {
    my $docker = test_docker('GET /containers/abc/stats' => $STOPPED);

    my @got;
    my $summary = eval {
      $docker->containers->stats('abc',
        stream   => 1,
        on_event => sub { push @got, $_[0] },
      );
    };
    my $err = $@;



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