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