466 lines
13 KiB
Perl
466 lines
13 KiB
Perl
#!/usr/bin/perl
|
|
# t/plack-subdomain.t
|
|
#
|
|
# Test subdomain function middleware (shop, support, mobile redirects/rewrites)
|
|
#
|
|
# Authors:
|
|
# Mark Smith <mark@dreamwidth.org>
|
|
#
|
|
# 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" );
|
|
};
|