Closes https://gitea.subcultureofone.org/greg/tkr/issues/43 Use a global $app dictionary to manage global state rather than having complex class constructors that expect three input arguments. Update and fix tests. Add tests for Util class functions that broke in the refactor. Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/44 Co-authored-by: Greg Sarjeant <greg@subcultureofone.org> Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
206 lines
9.5 KiB
PHP
206 lines
9.5 KiB
PHP
<?php
|
|
use PHPUnit\Framework\TestCase;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
|
|
final class UtilTest extends TestCase
|
|
{
|
|
// Define test date (strings) and expected outputs for
|
|
// testCanDisplayRelativeTime
|
|
public static function dateProvider(): array {
|
|
$datetime = new DateTimeImmutable();
|
|
|
|
return [
|
|
'1 minute ago' => [$datetime->modify('-1 minute')->format('c'), '1 minute ago'],
|
|
'2 hours ago' => [$datetime->modify('-2 hours')->format('c'), '2 hours ago'],
|
|
'3 days ago' => [$datetime->modify('-3 days')->format('c'), '3 days ago'],
|
|
'4 months ago' => [$datetime->modify('-4 months')->format('c'), '4 months ago'],
|
|
'5 years ago' => [$datetime->modify('-5 years')->format('c'), '5 years ago']
|
|
];
|
|
}
|
|
|
|
// Validate that the datetime strings provided by dateProvider
|
|
// yield the expected display strings
|
|
#[DataProvider('dateProvider')]
|
|
public function testCanDisplayRelativeTime(string $datetimeString, string $display): void {
|
|
$relativeTime = Util::relative_time($datetimeString);
|
|
$this->assertSame($relativeTime, $display);
|
|
}
|
|
|
|
public static function buildUrlProvider(): array {
|
|
return [
|
|
'basic path' => ['https://example.com', 'tkr', 'admin', 'https://example.com/tkr/admin'],
|
|
'baseUrl with trailing slash' => ['https://example.com/', 'tkr', 'admin', 'https://example.com/tkr/admin'],
|
|
'empty basePath' => ['https://example.com', '', 'admin', 'https://example.com/admin'],
|
|
'root basePath' => ['https://example.com', '/', 'admin', 'https://example.com/admin'],
|
|
'basePath no leading slash' => ['https://example.com', 'tkr', 'admin', 'https://example.com/tkr/admin'],
|
|
'basePath with leading slash' => ['https://example.com', '/tkr', 'admin', 'https://example.com/tkr/admin'],
|
|
'basePath with trailing slash' => ['https://example.com', 'tkr/', 'admin', 'https://example.com/tkr/admin'],
|
|
'basePath with both slashes' => ['https://example.com', '/tkr/', 'admin', 'https://example.com/tkr/admin'],
|
|
'complex path' => ['https://example.com', 'tkr', 'admin/css/upload', 'https://example.com/tkr/admin/css/upload'],
|
|
'path with leading slash' => ['https://example.com', 'tkr', '/admin', 'https://example.com/tkr/admin'],
|
|
'no path - empty basePath' => ['https://example.com', '', '', 'https://example.com/'],
|
|
'no path - root basePath' => ['https://example.com', '/', '', 'https://example.com/'],
|
|
'no path - tkr basePath' => ['https://example.com', 'tkr', '', 'https://example.com/tkr/'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('buildUrlProvider')]
|
|
public function testBuildUrl(string $baseUrl, string $basePath, string $path, string $expected): void {
|
|
$result = Util::buildUrl($baseUrl, $basePath, $path);
|
|
$this->assertEquals($expected, $result);
|
|
}
|
|
|
|
public static function buildRelativeUrlProvider(): array {
|
|
return [
|
|
'empty basePath with path' => ['', 'admin', '/admin'],
|
|
'root basePath with path' => ['/', 'admin', '/admin'],
|
|
'tkr basePath with path' => ['tkr', 'admin', '/tkr/admin'],
|
|
'tkr with leading slash' => ['/tkr', 'admin', '/tkr/admin'],
|
|
'tkr with trailing slash' => ['tkr/', 'admin', '/tkr/admin'],
|
|
'tkr with both slashes' => ['/tkr/', 'admin', '/tkr/admin'],
|
|
'complex path' => ['tkr', 'admin/css/upload', '/tkr/admin/css/upload'],
|
|
'path with leading slash' => ['tkr', '/admin', '/tkr/admin'],
|
|
'no path - empty basePath' => ['', '', '/'],
|
|
'no path - root basePath' => ['/', '', '/'],
|
|
'no path - tkr basePath' => ['tkr', '', '/tkr'],
|
|
'no path - tkr with slashes' => ['/tkr/', '', '/tkr'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('buildRelativeUrlProvider')]
|
|
public function testBuildRelativeUrl(string $basePath, string $path, string $expected): void {
|
|
$result = Util::buildRelativeUrl($basePath, $path);
|
|
$this->assertEquals($expected, $result);
|
|
}
|
|
|
|
// Test data for escape_html function
|
|
public static function escapeHtmlProvider(): array {
|
|
return [
|
|
'basic HTML' => ['<script>alert("xss")</script>', '<script>alert("xss")</script>'],
|
|
'quotes' => ['He said "Hello" & she said \'Hi\'', 'He said "Hello" & she said 'Hi''],
|
|
'empty string' => ['', ''],
|
|
'normal text' => ['Hello World', 'Hello World'],
|
|
'ampersand' => ['Tom & Jerry', 'Tom & Jerry'],
|
|
'unicode' => ['🚀 emoji & text', '🚀 emoji & text'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('escapeHtmlProvider')]
|
|
public function testEscapeHtml(string $input, string $expected): void {
|
|
$result = Util::escape_html($input);
|
|
$this->assertEquals($expected, $result);
|
|
}
|
|
|
|
// Test data for escape_xml function
|
|
public static function escapeXmlProvider(): array {
|
|
return [
|
|
'basic XML' => ['<tag attr="value">content</tag>', '<tag attr="value">content</tag>'],
|
|
'quotes and ampersand' => ['Title & "Subtitle"', 'Title & "Subtitle"'],
|
|
'empty string' => ['', ''],
|
|
'normal text' => ['Hello World', 'Hello World'],
|
|
'unicode' => ['🎵 music & notes', '🎵 music & notes'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('escapeXmlProvider')]
|
|
public function testEscapeXml(string $input, string $expected): void {
|
|
$result = Util::escape_xml($input);
|
|
$this->assertEquals($expected, $result);
|
|
}
|
|
|
|
// Test data for linkify function
|
|
public static function linkifyProvider(): array {
|
|
return [
|
|
'simple URL' => [
|
|
'Check out https://example.com for more info',
|
|
'Check out <a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a> for more info',
|
|
false // not strict accessibility
|
|
],
|
|
'URL with path' => [
|
|
'Visit https://example.com/path/to/page',
|
|
'Visit <a href="https://example.com/path/to/page" target="_blank" rel="noopener noreferrer">https://example.com/path/to/page</a>',
|
|
false
|
|
],
|
|
'multiple URLs' => [
|
|
'See https://example.com and https://other.com',
|
|
'See <a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a> and <a href="https://other.com" target="_blank" rel="noopener noreferrer">https://other.com</a>',
|
|
false
|
|
],
|
|
'URL with punctuation' => [
|
|
'Check https://example.com.',
|
|
'Check <a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>',
|
|
false
|
|
],
|
|
'no URL' => [
|
|
'Just some regular text',
|
|
'Just some regular text',
|
|
false
|
|
],
|
|
'strict accessibility mode' => [
|
|
'Visit https://example.com now',
|
|
'Visit <a tabindex="0" href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a> now',
|
|
true // strict accessibility
|
|
],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('linkifyProvider')]
|
|
public function testLinkify(string $input, string $expected, bool $strictAccessibility): void {
|
|
// Set up global $app with config
|
|
global $app;
|
|
$app = [
|
|
'config' => (object)['strictAccessibility' => $strictAccessibility]
|
|
];
|
|
|
|
$result = Util::linkify($input);
|
|
$this->assertEquals($expected, $result);
|
|
}
|
|
|
|
public function testLinkifyNoNewWindow(): void {
|
|
// Test linkify without new window
|
|
global $app;
|
|
$app = [
|
|
'config' => (object)['strictAccessibility' => false]
|
|
];
|
|
|
|
$input = 'Visit https://example.com';
|
|
$expected = 'Visit <a href="https://example.com">https://example.com</a>';
|
|
|
|
$result = Util::linkify($input, false); // no new window
|
|
$this->assertEquals($expected, $result);
|
|
}
|
|
|
|
public function testGetClientIp(): void {
|
|
// Test basic case with REMOTE_ADDR
|
|
$_SERVER['REMOTE_ADDR'] = '192.168.1.100';
|
|
unset($_SERVER['HTTP_CLIENT_IP'], $_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_X_REAL_IP']);
|
|
|
|
$result = Util::getClientIp();
|
|
$this->assertEquals('192.168.1.100', $result);
|
|
}
|
|
|
|
public function testGetClientIpWithForwardedHeaders(): void {
|
|
// Test precedence: HTTP_CLIENT_IP > HTTP_X_FORWARDED_FOR > HTTP_X_REAL_IP > REMOTE_ADDR
|
|
$_SERVER['HTTP_CLIENT_IP'] = '10.0.0.1';
|
|
$_SERVER['HTTP_X_FORWARDED_FOR'] = '10.0.0.2';
|
|
$_SERVER['HTTP_X_REAL_IP'] = '10.0.0.3';
|
|
$_SERVER['REMOTE_ADDR'] = '10.0.0.4';
|
|
|
|
$result = Util::getClientIp();
|
|
$this->assertEquals('10.0.0.1', $result); // Should use HTTP_CLIENT_IP
|
|
}
|
|
|
|
public function testGetClientIpUnknown(): void {
|
|
// Test when no IP is available
|
|
unset($_SERVER['HTTP_CLIENT_IP'], $_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_X_REAL_IP'], $_SERVER['REMOTE_ADDR']);
|
|
|
|
$result = Util::getClientIp();
|
|
$this->assertEquals('unknown', $result);
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
// Clean up $_SERVER after IP tests
|
|
unset($_SERVER['HTTP_CLIENT_IP'], $_SERVER['HTTP_X_FORWARDED_FOR'], $_SERVER['HTTP_X_REAL_IP'], $_SERVER['REMOTE_ADDR']);
|
|
}
|
|
|
|
} |