[$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>'],
'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' => ['content', '<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 https://example.com for more info',
false // not strict accessibility
],
'URL with path' => [
'Visit https://example.com/path/to/page',
'Visit https://example.com/path/to/page',
false
],
'multiple URLs' => [
'See https://example.com and https://other.com',
'See https://example.com and https://other.com',
false
],
'URL with punctuation' => [
'Check https://example.com.',
'Check https://example.com',
false
],
'no URL' => [
'Just some regular text',
'Just some regular text',
false
],
'strict accessibility mode' => [
'Visit https://example.com now',
'Visit https://example.com 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 https://example.com';
$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']);
}
}