mourningdove/t/plack-auth.t

245 lines
7.1 KiB
Perl
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
# t/plack-auth.t
#
# Tests for the Plack auth middleware (Plack::Middleware::DW::Auth).
# Uses mocking to simulate session cookie resolution without requiring 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 auth tests";
};
}
plan tests => 11;
# 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
my $mock_sessobj;
my $mock_load_user;
my $mock_bounce_url;
# Minimal mock session object
package MockSession;
sub new {
my ( $class, %opts ) = @_;
return bless \%opts, $class;
}
sub owner { return $_[0]->{owner} }
sub try_renew { }
package main;
# Minimal mock user object
package MockUser;
sub new {
my ( $class, %opts ) = @_;
return bless \%opts, $class;
}
sub user { return $_[0]->{user} }
sub userid { return $_[0]->{userid} }
sub note_activity { }
package main;
{
no warnings 'redefine', 'once';
# Mock routing: return the remote user as seen by the app
*DW::Routing::call = sub {
my ( $class, %args ) = @_;
my $r = DW::Request->get;
my $remote = $LJ::CACHE_REMOTE;
$r->status(200);
$r->header_out( 'Content-Type' => 'text/plain' );
if ($remote) {
$r->print( "user:" . $remote->user );
}
else {
$r->print("user:anonymous");
}
return;
};
*LJ::Session::session_from_cookies = sub {
my ( $class, %opts ) = @_;
# Simulate domain session bounce: if mock_bounce_url is set and
# session_from_cookies was called with redirect_ref, populate it
if ( $mock_bounce_url && $opts{redirect_ref} ) {
${ $opts{redirect_ref} } = $mock_bounce_url;
}
return $mock_sessobj;
};
*LJ::load_user = sub {
my ($username) = @_;
return $mock_load_user->($username) if $mock_load_user;
return undef;
};
# Disable sysban checks for auth tests
*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 };
# Prevent LJ::get_remote() from going through Login.pm's full path
# which requires a real LJ::User object. The Auth middleware sets
# $CACHE_REMOTE directly; we just need get_remote to return it.
*LJ::User::Login::get_remote = sub { return $LJ::CACHE_REMOTE };
}
# Helper to reset state
sub reset_mocks {
$mock_sessobj = undef;
$mock_load_user = undef;
$mock_bounce_url = undef;
@LJ::CLEANUP_HANDLERS = ();
$LJ::CACHED_REMOTE = 0;
$LJ::CACHE_REMOTE = undef;
$LJ::CACHE_REMOTE_BOUNCE_URL = "";
}
# Test 1: No session cookie -> anonymous
reset_mocks();
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 200, "No session cookie returns 200" );
};
# Test 2: No session cookie -> remote is anonymous
reset_mocks();
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
like( $res->content, qr/user:anonymous/, "No session cookie means anonymous user" );
};
# Test 3: Valid session cookie -> user is set as remote
reset_mocks();
my $mock_user = MockUser->new( user => 'testuser', userid => 42 );
$mock_sessobj = MockSession->new( owner => $mock_user );
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
like( $res->content, qr/user:testuser/, "Valid session sets remote to authenticated user" );
};
# Test 4: Session with no owner -> anonymous
reset_mocks();
$mock_sessobj = MockSession->new( owner => undef );
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
like( $res->content, qr/user:anonymous/, "Session with no owner means anonymous" );
};
# Test 5: Dev server ?as= parameter overrides remote
reset_mocks();
$mock_user = MockUser->new( user => 'testuser', userid => 42 );
$mock_sessobj = MockSession->new( owner => $mock_user );
my $dev_user = MockUser->new( user => 'devuser', userid => 43 );
$mock_load_user = sub { return $_[0] eq 'devuser' ? $dev_user : undef };
local $LJ::IS_DEV_SERVER = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/?as=devuser" );
like( $res->content, qr/user:devuser/, "Dev server ?as= overrides authenticated user" );
};
# Test 6: Dev server ?as= with invalid user -> logs out
reset_mocks();
$mock_user = MockUser->new( user => 'testuser', userid => 42 );
$mock_sessobj = MockSession->new( owner => $mock_user );
$mock_load_user = sub { return undef };
local $LJ::IS_DEV_SERVER = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/?as=nobody" );
like( $res->content, qr/user:anonymous/, "Dev server ?as= with invalid user logs out" );
};
# Test 7: ?as= without a value doesn't change remote
reset_mocks();
$mock_user = MockUser->new( user => 'testuser', userid => 42 );
$mock_sessobj = MockSession->new( owner => $mock_user );
local $LJ::IS_DEV_SERVER = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/?as=" );
like( $res->content, qr/user:testuser/, "?as= with empty value does not change remote" );
};
# Test 8: ?as= rejects invalid username format
reset_mocks();
$mock_user = MockUser->new( user => 'testuser', userid => 42 );
$mock_sessobj = MockSession->new( owner => $mock_user );
$mock_load_user = sub { return MockUser->new( user => 'badguy', userid => 99 ) };
local $LJ::IS_DEV_SERVER = 1;
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/?as=invalid!user" );
like( $res->content, qr/user:testuser/, "?as= with invalid username format is ignored" );
};
# Test 9: Stale domain session cookie triggers bounce redirect on GET
reset_mocks();
$mock_bounce_url =
"https://www.example.com/misc/get_domain_session?return=http%3A%2F%2Fmark.example.com%2F";
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( GET "/" );
is( $res->code, 303, "Stale domain session triggers redirect" );
is( $res->header('Location'),
$mock_bounce_url, "Redirect goes to get_domain_session bounce URL" );
};
# Test 10: Stale domain session does NOT redirect on POST
reset_mocks();
$mock_bounce_url =
"https://www.example.com/misc/get_domain_session?return=http%3A%2F%2Fmark.example.com%2F";
test_psgi $app, sub {
my $cb = shift;
my $res = $cb->( POST "/" );
isnt( $res->code, 303, "Stale domain session does not redirect POST requests" );
};