249 lines
6.9 KiB
Perl
249 lines
6.9 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
# t/plack-media.t
|
||
|
|
#
|
||
|
|
# Test media serving controllers (userpic, vgift, palimg) under Plack
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2026 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use warnings;
|
||
|
|
use v5.10;
|
||
|
|
|
||
|
|
use Test::More;
|
||
|
|
use HTTP::Request::Common;
|
||
|
|
use Plack::Test;
|
||
|
|
|
||
|
|
BEGIN {
|
||
|
|
require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
|
||
|
|
|
||
|
|
eval "use Plack::Test; 1" or do {
|
||
|
|
plan skip_all => "Plack::Test required for integration tests";
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
plan tests => 22;
|
||
|
|
|
||
|
|
# Load the Plack app
|
||
|
|
my $app_file = "$ENV{LJHOME}/app.psgi";
|
||
|
|
my $app = do $app_file;
|
||
|
|
die "Failed to load app.psgi: $@" if $@;
|
||
|
|
die "app.psgi did not return a code reference" unless $app && ref $app eq 'CODE';
|
||
|
|
|
||
|
|
# Disable middleware concerns not under test (auth, sysban, rate limiting)
|
||
|
|
{
|
||
|
|
no warnings 'redefine', 'once';
|
||
|
|
|
||
|
|
*LJ::Session::session_from_cookies = sub { return undef };
|
||
|
|
*LJ::sysban_check = sub { return 0 };
|
||
|
|
*LJ::Sysban::tempban_check = sub { return 0 };
|
||
|
|
*LJ::UniqCookie::parts_from_cookie = sub { return () };
|
||
|
|
*LJ::UniqCookie::ensure_cookie_value = sub { return };
|
||
|
|
*LJ::User::Login::get_remote = sub { return undef };
|
||
|
|
*DW::RateLimit::get = sub { return undef };
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---- Userpic tests ----
|
||
|
|
|
||
|
|
# Test 1: Userpic with If-Modified-Since returns 304
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $req = GET "/userpic/12345/1";
|
||
|
|
$req->header( 'If-Modified-Since' => 'Thu, 01 Jan 2026 00:00:00 GMT' );
|
||
|
|
my $res = $cb->($req);
|
||
|
|
|
||
|
|
is( $res->code, 304, "userpic returns 304 for If-Modified-Since" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 2: Userpic with invalid picid/userid returns 404 (no such user)
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/userpic/99999999/99999999" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "userpic returns 404 for nonexistent user/pic" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 3: Userpic with bad URL format returns 404
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/userpic/notanumber/1" );
|
||
|
|
|
||
|
|
isnt( $res->code, 200, "userpic rejects non-numeric picid" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 4: /userpics (without /userpic/) should not match userpic route
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/userpics" );
|
||
|
|
|
||
|
|
isnt( $res->code, 200, "/userpics does not match userpic route" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# ---- VGift tests ----
|
||
|
|
|
||
|
|
# Test 5: VGift with If-Modified-Since returns 304
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $req = GET "/vgift/12345/small";
|
||
|
|
$req->header( 'If-Modified-Since' => 'Thu, 01 Jan 2026 00:00:00 GMT' );
|
||
|
|
my $res = $cb->($req);
|
||
|
|
|
||
|
|
is( $res->code, 304, "vgift returns 304 for If-Modified-Since" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 6: VGift IMS from admin interface should NOT return 304
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $req = GET "/vgift/12345/small";
|
||
|
|
$req->header( 'If-Modified-Since' => 'Thu, 01 Jan 2026 00:00:00 GMT' );
|
||
|
|
$req->header( 'Referer' => "$LJ::SITEROOT/admin/vgifts" );
|
||
|
|
my $res = $cb->($req);
|
||
|
|
|
||
|
|
isnt( $res->code, 304, "vgift does not return 304 when referer is admin" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 7: VGift with invalid size returns 404
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/vgift/12345/medium" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "vgift returns 404 for invalid size" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 8: VGift with valid size format but nonexistent pic returns 404
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/vgift/99999999/large" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "vgift returns 404 for nonexistent pic" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# ---- PalImg tests ----
|
||
|
|
|
||
|
|
# Test 9: Basic palimg serves a real image
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.png" );
|
||
|
|
|
||
|
|
is( $res->code, 200, "palimg serves existing image" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 10: palimg has correct content type for PNG
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.png" );
|
||
|
|
|
||
|
|
is( $res->content_type, 'image/png', "palimg returns image/png for .png" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 11: palimg has correct content type for GIF
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/s1gradient.gif" );
|
||
|
|
|
||
|
|
is( $res->content_type, 'image/gif', "palimg returns image/gif for .gif" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 12: palimg returns ETag header
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.png" );
|
||
|
|
|
||
|
|
ok( $res->header('ETag'), "palimg response includes ETag" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 13: palimg returns Last-Modified header
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.png" );
|
||
|
|
|
||
|
|
ok( $res->header('Last-Modified'), "palimg response includes Last-Modified" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 14: palimg with matching ETag returns 304
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
|
||
|
|
# First request to get the ETag
|
||
|
|
my $res1 = $cb->( GET "/palimg/solid.png" );
|
||
|
|
my $etag = $res1->header('ETag');
|
||
|
|
|
||
|
|
# Second request with If-None-Match
|
||
|
|
my $req2 = GET "/palimg/solid.png";
|
||
|
|
$req2->header( 'If-None-Match' => $etag );
|
||
|
|
my $res2 = $cb->($req2);
|
||
|
|
|
||
|
|
is( $res2->code, 304, "palimg returns 304 for matching ETag" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 15: palimg returns 404 for nonexistent file
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/nonexistent.gif" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "palimg returns 404 for nonexistent file" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 16: palimg rejects path traversal
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/../etc/passwd.gif" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "palimg rejects path traversal" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 17: palimg rejects unsupported extension
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.jpg" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "palimg rejects non-gif/png extension" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 18: palimg with tint palette spec
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.png/pte0d0ff" );
|
||
|
|
|
||
|
|
is( $res->code, 200, "palimg with tint spec returns 200" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 19: palimg with gradient palette spec on GIF
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/s1gradient.gif/pg00ff000080ff8000" );
|
||
|
|
|
||
|
|
is( $res->code, 200, "palimg with gradient spec returns 200" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 20: palimg with invalid palette spec returns 404
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.png/pzzzinvalid" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "palimg with invalid palette spec returns 404" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 21: palimg with invalid extra path returns 404
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( GET "/palimg/solid.png/notpalette" );
|
||
|
|
|
||
|
|
is( $res->code, 404, "palimg with non-palette extra returns 404" );
|
||
|
|
};
|
||
|
|
|
||
|
|
# Test 22: palimg HEAD request returns 200 with no body
|
||
|
|
test_psgi $app, sub {
|
||
|
|
my $cb = shift;
|
||
|
|
my $res = $cb->( HEAD "/palimg/solid.png" );
|
||
|
|
|
||
|
|
is( $res->code, 200, "palimg HEAD returns 200" );
|
||
|
|
};
|