The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!perl
use strict;
use JSON;
use Pinto::Constants qw(:server :protocol);
use lib 't/lib';
use Pinto::Tester::Util qw(make_dist_archive);
#------------------------------------------------------------------------------
# Setup...
my $t = Pinto::Tester->new;
my %opts = ( root => $t->pinto->root );
my $app = Pinto::Server->new(%opts)->to_app;
my @headers = (Accept => $PINTO_PROTOCOL_ACCEPT);
#------------------------------------------------------------------------------
# Fetching an index...
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $req = GET('modules/02packages.details.txt.gz');
my $res = $cb->($req);
is $res->code, 200, 'Correct status code';
is $res->header('Content-Type'), 'application/x-gzip', 'Correct Type header';
cmp_ok $res->header('Content-Length'), '>', 4000, 'Reasonable Length header'; # Actual length may vary
cmp_ok $res->header('Content-Length'), '<', 7000, 'Reasonable Length header'; # Actual length may vary
is $res->header('Content-Length'), length $res->content, 'Length header matches actual length';
is $res->header('Cache-Control'), 'no-cache', 'Got a "Cache-Control: no-cache" header';
isnt str2time( $res->header('Last-Modified') ), undef, 'Last-Modified header contains a proper HTTP::Date string';
};
#------------------------------------------------------------------------------
# Test fetching legacy indexes (used by the cpan[1] client)
test_psgi
app => $app,
client => sub {
my $cb = shift;
my @paths = qw(authors/01mailrc.txt.gz modules/03modlist.data.gz);
for my $path (@paths) {
for my $prefix ( 'stacks/master/', '' ) {
my $url = $prefix . $path;
my $req = GET($url);
my $res = $cb->($req);
is $res->code, 200, "Got response for $url";
is $res->header('Cache-Control'), "no-cache", "$url got a 'Cache-Control: no-cache' header";
}
}
};
#------------------------------------------------------------------------------
# Add an archive, then fetch it back. Finally, check that all packages in the
# archive are present in the listing
subtest 'validate archive' => sub {
my $archive = make_dist_archive('TestDist-1.0=Foo~0.7; Bar~0.8')->stringify;
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $params = { author => 'THEBARD', recurse => 0, message => 'test', archives => [$archive] };
my $req = POST( 'action/add', @headers, Content => { action => encode_json($params) } );
my $res = $cb->($req);
action_response_ok($res);
};
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $params = { stack => 'master' };
my $req = POST( 'action/lock', @headers, Content => { action => encode_json($params) } );
my $res = $cb->($req);
action_response_ok($res);
};
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $params = { author => 'THEBARD', recurse => 0, message => 'test', archives => [$archive] };
my $req = POST( 'action/add', @headers, Content => { action => encode_json($params) } );
my $res = $cb->($req);
action_response_not_ok( $res, qr{is locked} );
};
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $url = 'stacks/master/authors/id/T/TH/THEBARD/TestDist-1.0.tar.gz';
my $req = GET($url);
my $res = $cb->($req);
is $res->code, 200, "Correct status code for GET $url";
is $res->header('Content-Type'), 'application/x-gzip', "Correct Type header for GET $url";
is $res->header('Content-Length'), -s $archive, "Length header matches actual archive size for GET $url";
is $res->header('Content-Length'), length $res->content,
"Length header matches actual content length for GET $url";
};
my $last_modified;
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $url = 'stacks/master/authors/id/T/TH/THEBARD/TestDist-1.0.tar.gz';
my $req = HEAD($url);
my $res = $cb->($req);
$last_modified = $res->header('Last-Modified');
isnt str2time($last_modified), undef, "Last-Modified header contains a proper HTTP::Date string for HEAD $url";
is $res->code, 200, "Correct status code for HEAD $url";
is $res->header('Content-Type'), 'application/x-gzip', "Correct Type header for HEAD $url";
is $res->header('Content-Length'), -s $archive, "Length header matches actual archive size for HEAD $url";
is length $res->content, 0, "No content returned for HEAD $url";
};
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $url = 'stacks/master/authors/id/T/TH/THEBARD/TestDist-1.0.tar.gz';
my $req = GET( $url, 'If-Modified-Since' => $last_modified );
my $res = $cb->($req);
is $res->code, 304, "Correct status code for unmodified $url";
is $res->header('Content-Type'), undef, "No Content-Type header for 304 response";
is $res->header('Content-Length'), undef, "No Content-Length header for 304 response";
is length $res->content, 0, "No content returned for 304 response";
};
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $params = {};
my $req = POST( 'action/list', @headers, Content => { action_args => encode_json($params) } );
my $res = $cb->($req);
is $res->code, 200, 'Correct status code';
# Note that the lines of the listing itself should NOT contain
# the $PINTO_PROTOCOL_DIAG_PREFIX in front of each line.
like $res->content, qr{\s Foo \s+ 0.7 \s+ \S+ \n}mx, 'Listing contains the Foo package';
like $res->content, qr{\s Bar \s+ 0.8 \s+ \S+ \n}mx, 'Listing contains the Bar package';
};
};
#------------------------------------------------------------------------------
# Make two stacks, add a different version of a dist to each stack, then fetch
# the index for each stack. The indexes should contain different dists.
for my $v ( 1, 2 ) {
my $stack = "stack_$v";
my $archive = make_dist_archive("Fruit-$v=Apple~$v; Orange~$v")->stringify;
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $params = { stack => $stack };
my $req = POST( 'action/new', @headers, Content => { action => encode_json($params) } );
my $res = $cb->($req);
action_response_ok($res);
};
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $params = { author => 'JOHN', recurse => 0, stack => $stack, message => 'test', archives => [$archive] };
my $req = POST( 'action/add', @headers, Content => { action => encode_json($params) } );
my $res = $cb->($req);
action_response_ok($res);
};
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $req = GET("stacks/$stack/modules/02packages.details.txt.gz");
my $res = $cb->($req);
is $res->code, 200, 'Correct status code';
# Write the index to a file
my $temp = File::Temp->new;
print {$temp} $res->content;
close $temp;
# Slurp index contents into memory
my $fh = IO::Zlib->new( $temp->filename, "rb" ) or die $!;
my $index = join '', <$fh>;
close $fh;
# Test index contents
for (qw(Apple Orange)) {
like $index, qr{^ $_ \s+ $v \s+ J/JO/JOHN/Fruit-$v.tar.gz $}mx, "index contains package $_-$v";
}
};
}
#------------------------------------------------------------------------------
# GET invalid path...
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $req = GET('bogus/path');
my $res = $cb->($req);
is $res->code, 404, 'Correct status code';
is $res->header('Content-Type'), 'text/plain';
is $res->header('Content-Length'), length $res->content;
like $res->content, qr{not found}i, 'File not found message';
};
#------------------------------------------------------------------------------
# POST invalid action
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $params = {};
my $req = POST( 'action/bogus', @headers, Content => { action => encode_json($params) } );
my $res = $cb->($req);
action_response_not_ok( $res, qr{Can't locate Pinto/Action/Bogus.pm}i );
};
#------------------------------------------------------------------------------
# Unversioned client (no Accept header)
test_psgi
app => $app,
client => sub {
my $cb = shift;
my $req = POST( 'action/nop', Content => { action => encode_json({}) } );
my $res = $cb->($req);
is $res->code, 415, 'Unsupported media type status';
like $res->content, qr/too old/;
like $res->content, qr/upgrade pinto/;
};
#------------------------------------------------------------------------------
# Client version is too old (i.e. server is too new)
test_psgi
app => $app,
client => sub {
my $cb = shift;
my @headers = (Accept => 'application/vnd.pinto.v0+text');
my $req = POST( 'action/nop', Content => { action => encode_json({}) } );
my $res = $cb->($req);
is $res->code, 415, 'Unsupported media type status';
like $res->content, qr/too old/;
like $res->content, qr/upgrade pinto/;
};
#------------------------------------------------------------------------------
# # Client version is too new (i.e. server is too old)
test_psgi
app => $app,
client => sub {
my $cb = shift;
my @headers = (Accept => 'application/vnd.pinto.v99+text');
my $req = POST( 'action/nop', @headers, Content => { action => encode_json({}) } );
my $res = $cb->($req);
is $res->code, 415, 'Unsupported media type status';
like $res->content, qr/too new/;
like $res->content, qr/upgrade pintod/;
};
#------------------------------------------------------------------------------
sub action_response_ok {
my ( $response, $pattern, $test_name ) = @_;
$test_name ||= sprintf '%s %s', $response->request->method, $response->request->uri;
# Report failues from caller's perspective
local $Test::Builder::Level = $Test::Builder::Level + 3;
my $type = $response->header('Content-Type');
is $type, 'text/plain', "Content-Type response header from $test_name";
my $status = $response->code;
is $status, 200, "Succesful status code for $test_name";
my $content = $response->content;
like $content, qr{$PINTO_PROTOCOL_STATUS_OK\n$}, "Response ends with status-ok for $test_name";
like $content, $pattern, "Response content matches for $test_name"
if $pattern;
}
#------------------------------------------------------------------------------
sub action_response_not_ok {
my ( $response, $pattern, $test_name ) = @_;
$test_name ||= sprintf '%s %s', $response->request->method, $response->request->uri;
# Report failues from caller's perspective
local $Test::Builder::Level = $Test::Builder::Level + 3;
my $type = $response->header('Content-Type');
is $type, 'text/plain', "Content-Type response header from $test_name";
my $status = $response->code;
is $status, 200, "Succesful status code for $test_name";
my $content = $response->content;
unlike $content, qr{$PINTO_PROTOCOL_STATUS_OK\n$}, "Response does not end with status-ok for $test_name";
like $content, $pattern, "Response content matches for $test_name"
if $pattern;
}
#------------------------------------------------------------------------------
done_testing;