244 lines
7.1 KiB
Perl
244 lines
7.1 KiB
Perl
#!/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" );
|
|
};
|