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