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 )