#!/usr/bin/perl # t/plack-subdomain.t # # Test subdomain function middleware (shop, support, mobile redirects/rewrites) # # Authors: # Mark Smith # # Copyright (c) 2026 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 integration tests"; }; } plan tests => 32; # 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'; # Stub routing and journal rendering so we can observe what reaches the app my $routed_uri; my $routed_username; my ( $journal_render_user, $journal_render_uri ); { no warnings 'redefine', 'once'; *DW::Routing::call = sub { my ( $class, %args ) = @_; $routed_uri = $args{uri} || ''; $routed_username = $args{username}; # When username is passed, use 'user' role — app-only routes like # the homepage (/) don't match, so return undef to let journal # rendering handle it. This mirrors real DW::Routing behavior. if ( $args{username} ) { return undef; } my $r = DW::Request->get; $r->status(200); $r->header_out( 'Content-Type' => 'text/plain' ); $r->print("routed:$routed_uri"); return 0; }; *DW::Controller::Journal::render = sub { my ( $class, %args ) = @_; $journal_render_user = $args{user}; $journal_render_uri = $args{uri}; my $r = DW::Request->get; $r->status(200); $r->header_out( 'Content-Type' => 'text/plain' ); $r->print("journal:$journal_render_user:$journal_render_uri"); return $r->res; }; # Disable middleware concerns not under test *LJ::Session::session_from_cookies = sub { return undef }; *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 }; *LJ::User::Login::get_remote = sub { return undef }; *DW::RateLimit::get = sub { return undef }; } # Configure subdomain functions for testing local $LJ::USER_DOMAIN = 'example.org'; local $LJ::DOMAIN_WEB = 'www.example.org'; local $LJ::DOMAIN = 'example.org'; local $LJ::SITEROOT = 'https://www.example.org'; local $LJ::PROTOCOL = 'https'; local %LJ::SUBDOMAIN_FUNCTION = ( shop => 'shop', support => 'support', mobile => 'mobile', ); # --- shop.example.org with SUBDOMAIN_FUNCTION{shop} = 'shop' --- # Should redirect to $SITEROOT/shop$uri # Test 1: shop subdomain redirects to /shop/randomgift test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/randomgift"; my $res = $cb->($req); is( $res->code, 303, "shop subdomain returns redirect" ); }; # Test 2: shop redirect Location is correct test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/randomgift"; my $res = $cb->($req); is( $res->header('Location'), 'https://www.example.org/shop/randomgift', "shop subdomain redirects to SITEROOT/shop/path" ); }; # Test 3: shop subdomain root redirects to /shop test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/"; my $res = $cb->($req); is( $res->header('Location'), 'https://www.example.org/shop', "shop subdomain root redirects to SITEROOT/shop (trailing slash stripped)" ); }; # Test 4: shop subdomain preserves query string test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/randomgift?type=paid"; my $res = $cb->($req); is( $res->header('Location'), 'https://www.example.org/shop/randomgift?type=paid', "shop subdomain redirect preserves query string" ); }; # --- support.example.org --- # Test 5: support subdomain redirects test_psgi $app, sub { my $cb = shift; my $req = GET "http://support.example.org/submit"; my $res = $cb->($req); is( $res->code, 303, "support subdomain returns redirect" ); }; # Test 6: support redirect goes to /support/ test_psgi $app, sub { my $cb = shift; my $req = GET "http://support.example.org/submit"; my $res = $cb->($req); is( $res->header('Location'), 'https://www.example.org/support/', "support subdomain redirects to SITEROOT/support/" ); }; # --- mobile.example.org --- # Test 7: mobile subdomain redirects test_psgi $app, sub { my $cb = shift; my $req = GET "http://mobile.example.org/read"; my $res = $cb->($req); is( $res->code, 303, "mobile subdomain returns redirect" ); }; # Test 8: mobile redirect preserves path test_psgi $app, sub { my $cb = shift; my $req = GET "http://mobile.example.org/read"; my $res = $cb->($req); is( $res->header('Location'), 'https://www.example.org/mobile/read', "mobile subdomain redirects to SITEROOT/mobile/path" ); }; # --- www.example.org (no subdomain function) --- # Test 9: www domain passes through without redirect test_psgi $app, sub { my $cb = shift; my $req = GET "http://www.example.org/shop/randomgift"; my $res = $cb->($req); is( $res->code, 200, "www domain request passes through (no redirect)" ); }; # Test 10: www domain routes to correct URI test_psgi $app, sub { my $cb = shift; my $req = GET "http://www.example.org/shop/randomgift"; $cb->($req); is( $routed_uri, '/shop/randomgift', "www domain routes to /shop/randomgift" ); }; # --- www.shop.example.org (www prefix on subdomain) --- # Test 11: www.shop.example.org redirects to drop www prefix test_psgi $app, sub { my $cb = shift; my $req = GET "http://www.shop.example.org/randomgift"; my $res = $cb->($req); is( $res->code, 303, "www.subdomain redirects" ); }; # Test 12: www.shop redirect drops www prefix test_psgi $app, sub { my $cb = shift; my $req = GET "http://www.shop.example.org/randomgift"; my $res = $cb->($req); is( $res->header('Location'), 'https://shop.example.org/randomgift', "www.shop.example.org redirects to shop.example.org" ); }; # --- shop subdomain with no SUBDOMAIN_FUNCTION entry (rewrite, not redirect) --- # Test 13-14: Without SUBDOMAIN_FUNCTION, shop subdomain rewrites URI inline { local %LJ::SUBDOMAIN_FUNCTION = (); # clear all functions test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/randomgift"; my $res = $cb->($req); is( $res->code, 200, "shop subdomain without func passes through (rewrite)" ); }; test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/randomgift"; $cb->($req); is( $routed_uri, '/shop/randomgift', "shop subdomain without func rewrites /randomgift to /shop/randomgift" ); }; } # --- shop rewrite strips trailing slash --- # Test 15-16: Rewrite mode strips trailing slash before prepending /shop { local %LJ::SUBDOMAIN_FUNCTION = (); test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/"; $cb->($req); is( $routed_uri, '/shop', "shop rewrite strips trailing slash from root" ); }; test_psgi $app, sub { my $cb = shift; my $req = GET "http://shop.example.org/cart/"; $cb->($req); is( $routed_uri, '/shop/cart', "shop rewrite strips trailing slash from path" ); }; } # --- User journal subdomain (no SUBDOMAIN_FUNCTION entry) --- # Test 17-18: username.example.org renders journal test_psgi $app, sub { my $cb = shift; $journal_render_user = undef; my $req = GET "http://someuser.example.org/2026/01/01/hello"; my $res = $cb->($req); is( $res->code, 200, "journal subdomain returns 200" ); }; test_psgi $app, sub { my $cb = shift; $journal_render_user = undef; $journal_render_uri = undef; my $req = GET "http://someuser.example.org/2026/01/01/hello"; $cb->($req); is( $journal_render_user, 'someuser', "journal subdomain passes username to render" ); }; # Test 19: journal subdomain passes path to render test_psgi $app, sub { my $cb = shift; $journal_render_uri = undef; my $req = GET "http://someuser.example.org/2026/01/01/hello"; $cb->($req); is( $journal_render_uri, '/2026/01/01/hello', "journal subdomain passes path to render" ); }; # Test 20: journal subdomain root test_psgi $app, sub { my $cb = shift; $journal_render_uri = undef; my $req = GET "http://someuser.example.org/"; $cb->($req); is( $journal_render_uri, '/', "journal subdomain root passes / to render" ); }; # --- "journal" SUBDOMAIN_FUNCTION (community, users, syndicated) --- # Test 21-22: journal function extracts user from path { local %LJ::SUBDOMAIN_FUNCTION = ( community => 'journal' ); test_psgi $app, sub { my $cb = shift; $journal_render_user = undef; $journal_render_uri = undef; my $req = GET "http://community.example.org/examplecomm/profile"; $cb->($req); is( $journal_render_user, 'examplecomm', "journal function extracts username from path" ); }; test_psgi $app, sub { my $cb = shift; $journal_render_uri = undef; my $req = GET "http://community.example.org/examplecomm/profile"; $cb->($req); is( $journal_render_uri, '/profile', "journal function extracts path after username" ); }; } # --- "normal" SUBDOMAIN_FUNCTION --- # Test 23-24: normal function passes through to app as-is { local %LJ::SUBDOMAIN_FUNCTION = ( somefunc => 'normal' ); test_psgi $app, sub { my $cb = shift; $routed_uri = undef; my $req = GET "http://somefunc.example.org/index"; $cb->($req); is( $routed_uri, '/index', "normal function passes URI through unchanged" ); }; test_psgi $app, sub { my $cb = shift; my $req = GET "http://somefunc.example.org/index"; my $res = $cb->($req); is( $res->code, 200, "normal function returns 200" ); }; } # --- changehost SUBDOMAIN_FUNCTION --- # Test 25-26: changehost redirects to new host { local %LJ::SUBDOMAIN_FUNCTION = ( old => [ 'changehost', 'new.example.com' ] ); test_psgi $app, sub { my $cb = shift; my $req = GET "http://old.example.org/some/path"; my $res = $cb->($req); is( $res->code, 303, "changehost returns redirect" ); }; test_psgi $app, sub { my $cb = shift; my $req = GET "http://old.example.org/some/path"; my $res = $cb->($req); is( $res->header('Location'), 'https://new.example.com/some/path', "changehost redirects to correct host" ); }; } # --- Username passed to routing for journal subdomains --- # Test 27: routing receives username for journal subdomains test_psgi $app, sub { my $cb = shift; $routed_username = undef; my $req = GET "http://someuser.example.org/2026/01/01/hello"; $cb->($req); is( $routed_username, 'someuser', "routing receives username for journal subdomain" ); }; # Test 28: routing receives no username for main domain test_psgi $app, sub { my $cb = shift; $routed_username = 'should-be-cleared'; my $req = GET "http://www.example.org/some/page"; $cb->($req); is( $routed_username, undef, "routing receives no username for main domain" ); }; # Test 29-30: journal subdomain root URI renders journal, not homepage test_psgi $app, sub { my $cb = shift; $journal_render_user = undef; my $req = GET "http://someuser.example.org/"; $cb->($req); is( $journal_render_user, 'someuser', "journal subdomain root URI renders journal (not homepage)" ); }; test_psgi $app, sub { my $cb = shift; $routed_username = undef; my $req = GET "http://someuser.example.org/"; $cb->($req); is( $routed_username, 'someuser', "journal subdomain root URI passes username to routing" ); }; # Test 31-32: main domain root URI renders homepage, not journal test_psgi $app, sub { my $cb = shift; $journal_render_user = undef; my $req = GET "http://www.example.org/"; my $res = $cb->($req); is( $journal_render_user, undef, "main domain root URI does not render journal" ); }; test_psgi $app, sub { my $cb = shift; my $req = GET "http://www.example.org/"; my $res = $cb->($req); like( $res->content, qr/^routed:/, "main domain root URI routes to homepage" ); };