API-Docker

 view release on metacpan or  search on metacpan

t/filters.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More;
use JSON::MaybeXS;
use API::Docker;

# What the `filters` query parameter looks like on the wire, and what shapes
# API::Docker::Role::Filters refuses to put there.
#
# Nothing here opens a socket or reaches a daemon, in either mode.
# Test::API::Docker::Mock is deliberately not used: under API_DOCKER_TEST_HOST
# it ignores its route table and hands back a real client, and almost
# everything asserted below is a property of the *outgoing* request, which no
# response can show. The daemon is faked under the socket instead, so the
# assertions hold on a machine with a daemon and on one without.
#
# The shapes come from measuring rootless Podman 5.4.2 (API 1.41) on
# 2026-08-27, GET /v1.41/images/json with a hand-built query string:
#
#   {"dangling":["true"]}   200, the dangling images
#   {"dangling":"true"}     500 json: cannot unmarshal string into Go value
#                               of type []string
#   {"dangling":true}       500 json: cannot unmarshal bool into Go value of
#                               type []string
#   {"dangling":[true]}     500 json: cannot unmarshal bool into Go value of
#                               type string
#   {"dangling":[1]}        500 json: cannot unmarshal number into Go value
#                               of type string
#   {"dangling":[null]}     500 non-boolean value ... strconv.ParseBool:
#                               parsing "" -- Go reads a JSON null into a
#                               string as ""
#   {"dangling":[""]}       500, the same message
#   {"dangling":["1"]}      200, and so do "0", "true" and "false"

# ---------------------------------------------------------------------------
# A client whose socket is an in-memory sink and whose response is canned, so
# _request assembles a real request line and query string with nothing on the
# other end. Same pattern as t/plugins.t and t/role_http.t.
package Test::Filters::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: $!";
  return $fh;
}

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

# The socket is lazy, so an unbuilt sink is the literal "no request was ever
# assembled" -- which is what the croak subtests below assert.
sub written {
  my ($self) = @_;
  my $sink = $self->_sink;
  return defined $sink ? $$sink : '';
}

package main;

sub fake_client {
  my ($body) = @_;
  return Test::Filters::FakeTransport->new(
    host        => 'unix:///nonexistent.sock',
    api_version => '1.41',
    canned      => [200, 'OK', {}, $body // '[]'],
  );
}

sub query_param {
  my ($raw, $name) = @_;
  my ($line) = $raw =~ /\A([^\r\n]*)/;
  my ($qs)   = $line =~ /\?([^ ]*) HTTP/;
  return undef unless defined $qs;
  for my $pair (split /&/, $qs) {
    my ($k, $v) = split /=/, $pair, 2;
    next unless $k eq $name;
    $v =~ s/%([0-9A-Fa-f]{2})/chr hex $1/ge;
    return $v;
  }
  return undef;
}

# The JSON *text* of the filters parameter, not a decoded structure: a decoded
# ["1"] and a decoded [1] are the same Perl scalar, and the difference between
# them is the whole point of this file.
sub sent_filters {
  my ($client) = @_;
  return query_param($client->written, 'filters');
}

# ---------------------------------------------------------------------------
# The shape that reaches the wire

subtest 'the correct shape travels unchanged' => sub {
  my $c = fake_client();
  $c->images->list(filters => { dangling => ['true'] });
  is sent_filters($c), '{"dangling":["true"]}',
    'a map of string to array of string is what the engine wants, and is left alone';
};

subtest 'a bare value is wrapped into the array the engine insists on' => sub {
  my $c = fake_client();
  $c->images->list(filters => { dangling => 'true' });
  is sent_filters($c), '{"dangling":["true"]}',
    'dangling => \'true\' meant one value; before this it went out as a bare '
    . 'string and the daemon answered 500 cannot unmarshal string into []string';
};

subtest 'a JSON boolean becomes the string the engine parses' => sub {
  my $c = fake_client();
  $c->images->list(filters => { dangling => JSON->true });
  is sent_filters($c), '{"dangling":["true"]}',
    'JSON->true would otherwise be encoded as a JSON true -- 500 cannot '
    . 'unmarshal bool into []string';

  my $f = fake_client();
  $f->images->list(filters => { dangling => JSON->false });
  is sent_filters($f), '{"dangling":["false"]}', 'and JSON->false likewise';

  my $s = fake_client();
  $s->images->list(filters => { dangling => [\1, \0] });
  is sent_filters($s), '{"dangling":["true","false"]}',



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