mourningdove/t/plack-sysban.t

202 lines
5.1 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
# t/plack-sysban.t
#
# Tests for the Plack sysban blocking middleware (Plack::Middleware::DW::Sysban).
# Uses mocking to simulate sysban checks without requiring memcache/DB.
#
# Authors:
# Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2025 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 sysban tests";
};
}
plan tests => 10;
# 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';
# Mock state: these control what the mocked functions return
my %mock_ip_bans;
my %mock_noanon_ip_bans;
my %mock_uniq_bans;
my $mock_tempban = 0;
my $mock_remote = undef;
my @mock_uniq_cookie;
{
no warnings 'redefine', 'once';
# Mock routing so requests that pass through sysban return 200.
# Return 0 to signal "handled" — returning undef falls through to BML.
*DW::Routing::call = sub {
my ( $class, %args ) = @_;
my $r = DW::Request->get;
$r->status(200);
$r->print('OK');
return 0;
};
*LJ::sysban_check = sub {
my ( $what, $value ) = @_;
return $mock_ip_bans{$value} if $what eq 'ip';
return $mock_noanon_ip_bans{$value} if $what eq 'noanon_ip';
return $mock_uniq_bans{$value} if $what eq 'uniq';
return 0;
};
*LJ::Sysban::tempban_check = sub {
return $mock_tempban;
};
*LJ::get_remote = sub {
return $mock_remote;
};
*LJ::UniqCookie::parts_from_cookie = sub {
return @mock_uniq_cookie;
};
# Disable middleware concerns not under test
*LJ::Session::session_from_cookies = sub { return undef };
*LJ::UniqCookie::ensure_cookie_value = sub { return };
*LJ::User::Login::get_remote = sub { return $mock_remote };
*DW::RateLimit::get = sub { return undef };
}
# Helper to reset all mocks
sub reset_mocks {
%mock_ip_bans = ();
%mock_noanon_ip_bans = ();
%mock_uniq_bans = ();
$mock_tempban = 0;
$mock_remote = undef;
@mock_uniq_cookie = ();
}
# Test 1: Normal request passes through
reset_mocks();
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 200, "Normal request passes through sysban" );
};
# Test 2: IP-banned request returns 403
reset_mocks();
$mock_ip_bans{'127.0.0.1'} = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 403, "IP-banned request returns 403" );
};
# Test 3: IP ban response contains blocked message
reset_mocks();
$mock_ip_bans{'127.0.0.1'} = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
like( $res->content, qr/403 Denied/, "IP ban response contains denied message" );
};
# Test 4: Tempban returns 403
reset_mocks();
$mock_tempban = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 403, "Tempbanned request returns 403" );
};
# Test 5: Uniq cookie ban returns 403
reset_mocks();
@mock_uniq_cookie = ( 'baduniq123', time(), '' );
$mock_uniq_bans{baduniq123} = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 403, "Uniq-banned request returns 403" );
};
# Test 6: Uniq cookie present but not banned passes through
reset_mocks();
@mock_uniq_cookie = ( 'gooduniq456', time(), '' );
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 200, "Uniq cookie present but not banned passes through" );
};
# Test 7: noanon_ip ban blocks anonymous user
reset_mocks();
$mock_remote = undef;
$mock_noanon_ip_bans{'127.0.0.1'} = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 403, "noanon_ip ban blocks anonymous user" );
};
# Test 8: noanon_ip ban response contains login link
reset_mocks();
$mock_remote = undef;
$mock_noanon_ip_bans{'127.0.0.1'} = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
like( $res->content, qr{/login}, "noanon_ip ban response contains login link" );
};
# Test 9: noanon_ip ban does NOT block logged-in user
reset_mocks();
$mock_remote = 1;
$mock_noanon_ip_bans{'127.0.0.1'} = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 200, "noanon_ip ban does not block logged-in user" );
};
# Test 10: noanon_ip ban allows /login path for anonymous user
reset_mocks();
$mock_remote = undef;
$mock_noanon_ip_bans{'127.0.0.1'} = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/login" );
is( $res->code, 200, "noanon_ip ban allows /login for anonymous user" );
};