1c176b8b3SJohann Duscher<?php 2c176b8b3SJohann Duscher/** 3c176b8b3SJohann Duscher * Delete Page Guard Plugin - Developer Test Suite 4c176b8b3SJohann Duscher * 5c176b8b3SJohann Duscher * This is a standalone test runner for developers to verify the plugin's 6c176b8b3SJohann Duscher * core functionality without requiring DokuWiki integration. 7c176b8b3SJohann Duscher * 8c176b8b3SJohann Duscher * Usage: php tests/test_runner.php 9c176b8b3SJohann Duscher * 10c176b8b3SJohann Duscher * @license GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html) - see LICENSE.md 11c176b8b3SJohann Duscher * @author Johann Duscher <jonny.dee@posteo.net> 12c176b8b3SJohann Duscher * @copyright 2025 Johann Duscher 13c176b8b3SJohann Duscher */ 14c176b8b3SJohann Duscher 15c176b8b3SJohann Duscher// Simple test framework 16c176b8b3SJohann Duscherclass TestRunner { 17c176b8b3SJohann Duscher private $tests = []; 18c176b8b3SJohann Duscher private $passed = 0; 19c176b8b3SJohann Duscher private $failed = 0; 20c176b8b3SJohann Duscher 21c176b8b3SJohann Duscher public function addTest($name, $callback) { 22c176b8b3SJohann Duscher $this->tests[] = ['name' => $name, 'callback' => $callback]; 23c176b8b3SJohann Duscher } 24c176b8b3SJohann Duscher 25c176b8b3SJohann Duscher public function run() { 26c176b8b3SJohann Duscher echo "Delete Page Guard Plugin - Developer Test Suite\n"; 27c176b8b3SJohann Duscher echo "================================================\n\n"; 28c176b8b3SJohann Duscher 29c176b8b3SJohann Duscher foreach ($this->tests as $test) { 30c176b8b3SJohann Duscher echo "Testing: " . $test['name'] . " ... "; 31c176b8b3SJohann Duscher 32c176b8b3SJohann Duscher try { 33c176b8b3SJohann Duscher $result = call_user_func($test['callback']); 34c176b8b3SJohann Duscher if ($result === true) { 35c176b8b3SJohann Duscher echo "✅ PASS\n"; 36c176b8b3SJohann Duscher $this->passed++; 37c176b8b3SJohann Duscher } else { 38c176b8b3SJohann Duscher echo "❌ FAIL: " . ($result ?: 'Unknown error') . "\n"; 39c176b8b3SJohann Duscher $this->failed++; 40c176b8b3SJohann Duscher } 41c176b8b3SJohann Duscher } catch (Exception $e) { 42c176b8b3SJohann Duscher echo "❌ ERROR: " . $e->getMessage() . "\n"; 43c176b8b3SJohann Duscher $this->failed++; 44c176b8b3SJohann Duscher } 45c176b8b3SJohann Duscher } 46c176b8b3SJohann Duscher 47c176b8b3SJohann Duscher echo "\n" . str_repeat("=", 50) . "\n"; 48c176b8b3SJohann Duscher echo "Results: {$this->passed} passed, {$this->failed} failed\n"; 49c176b8b3SJohann Duscher 50c176b8b3SJohann Duscher if ($this->failed === 0) { 51c176b8b3SJohann Duscher echo " All tests passed!\n"; 52c176b8b3SJohann Duscher exit(0); 53c176b8b3SJohann Duscher } else { 54c176b8b3SJohann Duscher echo " Some tests failed!\n"; 55c176b8b3SJohann Duscher exit(1); 56c176b8b3SJohann Duscher } 57c176b8b3SJohann Duscher } 58c176b8b3SJohann Duscher} 59c176b8b3SJohann Duscher 60c176b8b3SJohann Duscher// Include the test adapter 61c176b8b3SJohann Duscherrequire_once __DIR__ . '/plugin_test_adapter.php'; 62c176b8b3SJohann Duscher 63c176b8b3SJohann Duscher// Initialize test runner 64c176b8b3SJohann Duscher$runner = new TestRunner(); 65c176b8b3SJohann Duscher 66c176b8b3SJohann Duscher// Test 1: Pattern Validation - Valid Patterns 67c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - Valid Simple Pattern', function() { 68c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 69c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('^start$'); 70c176b8b3SJohann Duscher return $result === true; 71c176b8b3SJohann Duscher}); 72c176b8b3SJohann Duscher 73c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - Valid Complex Pattern', function() { 74c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 75c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('^users:[^:]+:(start|profile)$'); 76c176b8b3SJohann Duscher return $result === true; 77c176b8b3SJohann Duscher}); 78c176b8b3SJohann Duscher 79c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - Valid Namespace Pattern', function() { 80c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 81c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('^wiki:.*$'); 82c176b8b3SJohann Duscher return $result === true; 83c176b8b3SJohann Duscher}); 84c176b8b3SJohann Duscher 85c176b8b3SJohann Duscher// Test 2: Pattern Validation - Invalid Patterns 86c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - Invalid Syntax', function() { 87c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 88c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('[invalid'); 89c176b8b3SJohann Duscher return is_string($result) && strpos($result, 'invalid syntax') !== false; 90c176b8b3SJohann Duscher}); 91c176b8b3SJohann Duscher 92c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - ReDoS Protection', function() { 93c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 94c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('(a+)+b'); 95c176b8b3SJohann Duscher return is_string($result) && strpos($result, 'performance issues') !== false; 96c176b8b3SJohann Duscher}); 97c176b8b3SJohann Duscher 98c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - Another ReDoS Pattern', function() { 99c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 100c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('(x+)*y'); 101c176b8b3SJohann Duscher return is_string($result) && strpos($result, 'performance issues') !== false; 102c176b8b3SJohann Duscher}); 103c176b8b3SJohann Duscher 1049a383d51SJohann Duscher$runner->addTest('Pattern Validation - ReDoS Simple Plus Pattern', function() { 1059a383d51SJohann Duscher $plugin = new TestableDeletePageGuard(); 1069a383d51SJohann Duscher $result = $plugin->validateRegexPattern('(a+)+'); 1079a383d51SJohann Duscher return is_string($result) && strpos($result, 'performance issues') !== false; 1089a383d51SJohann Duscher}); 1099a383d51SJohann Duscher 1109a383d51SJohann Duscher$runner->addTest('Pattern Validation - ReDoS Simple Star Pattern', function() { 1119a383d51SJohann Duscher $plugin = new TestableDeletePageGuard(); 1129a383d51SJohann Duscher $result = $plugin->validateRegexPattern('(x*)*'); 1139a383d51SJohann Duscher return is_string($result) && strpos($result, 'performance issues') !== false; 1149a383d51SJohann Duscher}); 1159a383d51SJohann Duscher 116c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - Length Limit', function() { 117c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 118c176b8b3SJohann Duscher $longPattern = str_repeat('a', 1001); 119c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern($longPattern); 120c176b8b3SJohann Duscher return is_string($result) && strpos($result, 'too long') !== false; 121c176b8b3SJohann Duscher}); 122c176b8b3SJohann Duscher 123c176b8b3SJohann Duscher$runner->addTest('Pattern Validation - Line Number Reporting', function() { 124c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 125c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('[invalid', 5); 126c176b8b3SJohann Duscher return is_string($result) && strpos($result, 'Line 5:') !== false; 127c176b8b3SJohann Duscher}); 128c176b8b3SJohann Duscher 129c176b8b3SJohann Duscher// Test 3: Pattern Matching 130c176b8b3SJohann Duscher$runner->addTest('Pattern Matching - Exact Match', function() { 131c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 132c176b8b3SJohann Duscher return $plugin->matchesPattern('^start$', 'start') === true; 133c176b8b3SJohann Duscher}); 134c176b8b3SJohann Duscher 135c176b8b3SJohann Duscher$runner->addTest('Pattern Matching - No Match', function() { 136c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 137c176b8b3SJohann Duscher return $plugin->matchesPattern('^start$', 'other') === false; 138c176b8b3SJohann Duscher}); 139c176b8b3SJohann Duscher 140c176b8b3SJohann Duscher$runner->addTest('Pattern Matching - Complex Pattern Match', function() { 141c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 142c176b8b3SJohann Duscher return $plugin->matchesPattern('^users:[^:]+:start$', 'users:alice:start') === true; 143c176b8b3SJohann Duscher}); 144c176b8b3SJohann Duscher 145c176b8b3SJohann Duscher$runner->addTest('Pattern Matching - Complex Pattern No Match', function() { 146c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 147c176b8b3SJohann Duscher return $plugin->matchesPattern('^users:[^:]+:start$', 'users:alice:profile') === false; 148c176b8b3SJohann Duscher}); 149c176b8b3SJohann Duscher 150c176b8b3SJohann Duscher$runner->addTest('Pattern Matching - Partial Match', function() { 151c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 152c176b8b3SJohann Duscher return $plugin->matchesPattern('wiki', 'wiki:syntax') === true; 153c176b8b3SJohann Duscher}); 154c176b8b3SJohann Duscher 155c176b8b3SJohann Duscher$runner->addTest('Pattern Matching - Case Sensitive', function() { 156c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 157c176b8b3SJohann Duscher return $plugin->matchesPattern('^Wiki$', 'wiki') === false; 158c176b8b3SJohann Duscher}); 159c176b8b3SJohann Duscher 160c176b8b3SJohann Duscher// Test 4: File Path Conversion 161c176b8b3SJohann Duscher$runner->addTest('File Path Conversion - Standard Path', function() { 162c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 163c176b8b3SJohann Duscher $result = $plugin->getRelativeFilePath('/var/www/data/pages/namespace/page.txt', '/var/www/data'); 164c176b8b3SJohann Duscher return $result === 'namespace/page.txt'; 165c176b8b3SJohann Duscher}); 166c176b8b3SJohann Duscher 167c176b8b3SJohann Duscher$runner->addTest('File Path Conversion - Nested Path', function() { 168c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 169c176b8b3SJohann Duscher $result = $plugin->getRelativeFilePath('/var/www/data/pages/ns1/ns2/page.txt', '/var/www/data'); 170c176b8b3SJohann Duscher return $result === 'ns1/ns2/page.txt'; 171c176b8b3SJohann Duscher}); 172c176b8b3SJohann Duscher 173c176b8b3SJohann Duscher$runner->addTest('File Path Conversion - Windows Path', function() { 174c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 175c176b8b3SJohann Duscher $result = $plugin->getRelativeFilePath('C:\\dokuwiki\\data\\pages\\test\\page.txt', 'C:\\dokuwiki\\data'); 176c176b8b3SJohann Duscher return $result === 'test/page.txt'; 177c176b8b3SJohann Duscher}); 178c176b8b3SJohann Duscher 179c176b8b3SJohann Duscher$runner->addTest('File Path Conversion - No Pages Subdirectory', function() { 180c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 181c176b8b3SJohann Duscher $result = $plugin->getRelativeFilePath('/var/www/data/other/file.txt', '/var/www/data'); 182c176b8b3SJohann Duscher return $result === 'other/file.txt'; 183c176b8b3SJohann Duscher}); 184c176b8b3SJohann Duscher 185c176b8b3SJohann Duscher// Test 5: Configuration Parsing 186c176b8b3SJohann Duscher$runner->addTest('Configuration Parsing - Multiple Patterns', function() { 187c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 188c176b8b3SJohann Duscher $patterns = "^start$\n^sidebar$\n^users:[^:]+:start$"; 189c176b8b3SJohann Duscher $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 190c176b8b3SJohann Duscher return count($lines) === 3; 191c176b8b3SJohann Duscher}); 192c176b8b3SJohann Duscher 193c176b8b3SJohann Duscher$runner->addTest('Configuration Parsing - Empty Lines Ignored', function() { 194c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 195c176b8b3SJohann Duscher $patterns = "^start$\n\n\n^sidebar$\n \n^end$"; 196c176b8b3SJohann Duscher $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 197c176b8b3SJohann Duscher $nonEmpty = array_filter($lines, function($line) { return trim($line) !== ''; }); 198c176b8b3SJohann Duscher return count($nonEmpty) === 3; 199c176b8b3SJohann Duscher}); 200c176b8b3SJohann Duscher 201c176b8b3SJohann Duscher$runner->addTest('Configuration Parsing - Windows Line Endings', function() { 202c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 203c176b8b3SJohann Duscher $patterns = "^start$\r\n^sidebar$\r\n^end$"; 204c176b8b3SJohann Duscher $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 205c176b8b3SJohann Duscher return count($lines) === 3; 206c176b8b3SJohann Duscher}); 207c176b8b3SJohann Duscher 208c176b8b3SJohann Duscher// Test 6: Security Features 209c176b8b3SJohann Duscher$runner->addTest('Security - Forward Slash Escaping', function() { 210c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 211c176b8b3SJohann Duscher // Pattern with forward slashes should be properly escaped 212c176b8b3SJohann Duscher return $plugin->matchesPattern('path/to/file', 'path/to/file') === true; 213c176b8b3SJohann Duscher}); 214c176b8b3SJohann Duscher 215c176b8b3SJohann Duscher$runner->addTest('Security - Unicode Support', function() { 216c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 217c176b8b3SJohann Duscher // Test unicode pattern matching 218c176b8b3SJohann Duscher return $plugin->matchesPattern('^café$', 'café') === true; 219c176b8b3SJohann Duscher}); 220c176b8b3SJohann Duscher 221c176b8b3SJohann Duscher$runner->addTest('Security - Special Regex Characters', function() { 222c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 223c176b8b3SJohann Duscher // Test that dots are treated as literal dots when escaped 224c176b8b3SJohann Duscher return $plugin->matchesPattern('file\.txt$', 'file.txt') === true; 225c176b8b3SJohann Duscher}); 226c176b8b3SJohann Duscher 227c176b8b3SJohann Duscher$runner->addTest('Security - Injection Protection', function() { 228c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 229c176b8b3SJohann Duscher // Ensure patterns with potential injection attempts are handled safely 230c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern('(?{`ls`})'); 231c176b8b3SJohann Duscher return is_string($result); // Should fail validation, not execute code 232c176b8b3SJohann Duscher}); 233c176b8b3SJohann Duscher 234c176b8b3SJohann Duscher// Test 7: Edge Cases 235c176b8b3SJohann Duscher$runner->addTest('Edge Cases - Empty Pattern', function() { 236c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 237c176b8b3SJohann Duscher $result = $plugin->validateRegexPattern(''); 238c176b8b3SJohann Duscher // Empty patterns should be considered invalid 239c176b8b3SJohann Duscher return is_string($result) || $plugin->matchesPattern('', 'anything') === false; 240c176b8b3SJohann Duscher}); 241c176b8b3SJohann Duscher 242c176b8b3SJohann Duscher$runner->addTest('Edge Cases - Empty Target', function() { 243c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 244c176b8b3SJohann Duscher return $plugin->matchesPattern('^$', '') === true; 245c176b8b3SJohann Duscher}); 246c176b8b3SJohann Duscher 247c176b8b3SJohann Duscher$runner->addTest('Edge Cases - Whitespace Pattern', function() { 248c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 249c176b8b3SJohann Duscher return $plugin->matchesPattern('^\s+$', ' ') === true; 250c176b8b3SJohann Duscher}); 251c176b8b3SJohann Duscher 252c176b8b3SJohann Duscher$runner->addTest('Edge Cases - Very Long Target', function() { 253c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 254c176b8b3SJohann Duscher $longTarget = str_repeat('a', 10000); 255c176b8b3SJohann Duscher return $plugin->matchesPattern('^a+$', $longTarget) === true; 256c176b8b3SJohann Duscher}); 257c176b8b3SJohann Duscher 258c176b8b3SJohann Duscher// Test 8: Real-world Patterns 259c176b8b3SJohann Duscher$runner->addTest('Real-world - User Page Protection', function() { 260c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 261c176b8b3SJohann Duscher $pattern = '^users:[^:]+:start$'; 262c176b8b3SJohann Duscher return $plugin->matchesPattern($pattern, 'users:john:start') === true && 263c176b8b3SJohann Duscher $plugin->matchesPattern($pattern, 'users:mary:start') === true && 264c176b8b3SJohann Duscher $plugin->matchesPattern($pattern, 'users:admin:profile') === false; 265c176b8b3SJohann Duscher}); 266c176b8b3SJohann Duscher 267c176b8b3SJohann Duscher$runner->addTest('Real-world - Namespace Protection', function() { 268c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 269c176b8b3SJohann Duscher $pattern = '^admin:.*$'; 270c176b8b3SJohann Duscher return $plugin->matchesPattern($pattern, 'admin:config') === true && 271c176b8b3SJohann Duscher $plugin->matchesPattern($pattern, 'admin:users:list') === true && 272c176b8b3SJohann Duscher $plugin->matchesPattern($pattern, 'public:page') === false; 273c176b8b3SJohann Duscher}); 274c176b8b3SJohann Duscher 275c176b8b3SJohann Duscher$runner->addTest('Real-world - File Extension Pattern', function() { 276c176b8b3SJohann Duscher $plugin = new TestableDeletePageGuard(); 277c176b8b3SJohann Duscher $pattern = '\.txt$'; 278c176b8b3SJohann Duscher return $plugin->matchesPattern($pattern, 'document.txt') === true && 279c176b8b3SJohann Duscher $plugin->matchesPattern($pattern, 'image.png') === false; 280c176b8b3SJohann Duscher}); 281c176b8b3SJohann Duscher 282*bacaf983SJohann Duscher// Test: getMatchTarget method 283*bacaf983SJohann Duscher$runner->addTest('Match Target - Page ID Mode', function() { 284*bacaf983SJohann Duscher $plugin = new TestableDeletePageGuard(); 285*bacaf983SJohann Duscher $plugin->setTestConfig('match_target', 'id'); 286*bacaf983SJohann Duscher $result = $plugin->getMatchTarget('wiki:syntax'); 287*bacaf983SJohann Duscher return $result === 'wiki:syntax'; // Should return page ID unchanged 288*bacaf983SJohann Duscher}); 289*bacaf983SJohann Duscher 290*bacaf983SJohann Duscher$runner->addTest('Match Target - File Path Mode', function() { 291*bacaf983SJohann Duscher $plugin = new TestableDeletePageGuard(); 292*bacaf983SJohann Duscher $plugin->setTestConfig('match_target', 'filepath'); 293*bacaf983SJohann Duscher $result = $plugin->getMatchTarget('wiki:syntax'); 294*bacaf983SJohann Duscher // Should return relative file path 295*bacaf983SJohann Duscher return strpos($result, 'wiki/syntax.txt') !== false; 296*bacaf983SJohann Duscher}); 297*bacaf983SJohann Duscher 298*bacaf983SJohann Duscher$runner->addTest('Match Target - Nested Page in File Path Mode', function() { 299*bacaf983SJohann Duscher $plugin = new TestableDeletePageGuard(); 300*bacaf983SJohann Duscher $plugin->setTestConfig('match_target', 'filepath'); 301*bacaf983SJohann Duscher $result = $plugin->getMatchTarget('users:john:start'); 302*bacaf983SJohann Duscher return strpos($result, 'users/john/start.txt') !== false; 303*bacaf983SJohann Duscher}); 304*bacaf983SJohann Duscher 305*bacaf983SJohann Duscher// Test: Public API method exposure 306*bacaf983SJohann Duscher$runner->addTest('API - validateRegexPattern is public', function() { 307*bacaf983SJohann Duscher $plugin = new TestableDeletePageGuard(); 308*bacaf983SJohann Duscher $reflection = new ReflectionMethod($plugin, 'validateRegexPattern'); 309*bacaf983SJohann Duscher return $reflection->isPublic(); 310*bacaf983SJohann Duscher}); 311*bacaf983SJohann Duscher 312*bacaf983SJohann Duscher$runner->addTest('API - matchesPattern is public', function() { 313*bacaf983SJohann Duscher $plugin = new TestableDeletePageGuard(); 314*bacaf983SJohann Duscher $reflection = new ReflectionMethod($plugin, 'matchesPattern'); 315*bacaf983SJohann Duscher return $reflection->isPublic(); 316*bacaf983SJohann Duscher}); 317*bacaf983SJohann Duscher 318*bacaf983SJohann Duscher$runner->addTest('API - getMatchTarget is public', function() { 319*bacaf983SJohann Duscher $plugin = new TestableDeletePageGuard(); 320*bacaf983SJohann Duscher $reflection = new ReflectionMethod($plugin, 'getMatchTarget'); 321*bacaf983SJohann Duscher return $reflection->isPublic(); 322*bacaf983SJohann Duscher}); 323*bacaf983SJohann Duscher 324c176b8b3SJohann Duscher// Run all tests 325c176b8b3SJohann Duscher$runner->run();