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