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