#!/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 # # 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" ); };