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 - Length Limit', function() { 105 $plugin = new TestableDeletePageGuard(); 106 $longPattern = str_repeat('a', 1001); 107 $result = $plugin->validateRegexPattern($longPattern); 108 return is_string($result) && strpos($result, 'too long') !== false; 109}); 110 111$runner->addTest('Pattern Validation - Line Number Reporting', function() { 112 $plugin = new TestableDeletePageGuard(); 113 $result = $plugin->validateRegexPattern('[invalid', 5); 114 return is_string($result) && strpos($result, 'Line 5:') !== false; 115}); 116 117// Test 3: Pattern Matching 118$runner->addTest('Pattern Matching - Exact Match', function() { 119 $plugin = new TestableDeletePageGuard(); 120 return $plugin->matchesPattern('^start$', 'start') === true; 121}); 122 123$runner->addTest('Pattern Matching - No Match', function() { 124 $plugin = new TestableDeletePageGuard(); 125 return $plugin->matchesPattern('^start$', 'other') === false; 126}); 127 128$runner->addTest('Pattern Matching - Complex Pattern Match', function() { 129 $plugin = new TestableDeletePageGuard(); 130 return $plugin->matchesPattern('^users:[^:]+:start$', 'users:alice:start') === true; 131}); 132 133$runner->addTest('Pattern Matching - Complex Pattern No Match', function() { 134 $plugin = new TestableDeletePageGuard(); 135 return $plugin->matchesPattern('^users:[^:]+:start$', 'users:alice:profile') === false; 136}); 137 138$runner->addTest('Pattern Matching - Partial Match', function() { 139 $plugin = new TestableDeletePageGuard(); 140 return $plugin->matchesPattern('wiki', 'wiki:syntax') === true; 141}); 142 143$runner->addTest('Pattern Matching - Case Sensitive', function() { 144 $plugin = new TestableDeletePageGuard(); 145 return $plugin->matchesPattern('^Wiki$', 'wiki') === false; 146}); 147 148// Test 4: File Path Conversion 149$runner->addTest('File Path Conversion - Standard Path', function() { 150 $plugin = new TestableDeletePageGuard(); 151 $result = $plugin->getRelativeFilePath('/var/www/data/pages/namespace/page.txt', '/var/www/data'); 152 return $result === 'namespace/page.txt'; 153}); 154 155$runner->addTest('File Path Conversion - Nested Path', function() { 156 $plugin = new TestableDeletePageGuard(); 157 $result = $plugin->getRelativeFilePath('/var/www/data/pages/ns1/ns2/page.txt', '/var/www/data'); 158 return $result === 'ns1/ns2/page.txt'; 159}); 160 161$runner->addTest('File Path Conversion - Windows Path', function() { 162 $plugin = new TestableDeletePageGuard(); 163 $result = $plugin->getRelativeFilePath('C:\\dokuwiki\\data\\pages\\test\\page.txt', 'C:\\dokuwiki\\data'); 164 return $result === 'test/page.txt'; 165}); 166 167$runner->addTest('File Path Conversion - No Pages Subdirectory', function() { 168 $plugin = new TestableDeletePageGuard(); 169 $result = $plugin->getRelativeFilePath('/var/www/data/other/file.txt', '/var/www/data'); 170 return $result === 'other/file.txt'; 171}); 172 173// Test 5: Configuration Parsing 174$runner->addTest('Configuration Parsing - Multiple Patterns', function() { 175 $plugin = new TestableDeletePageGuard(); 176 $patterns = "^start$\n^sidebar$\n^users:[^:]+:start$"; 177 $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 178 return count($lines) === 3; 179}); 180 181$runner->addTest('Configuration Parsing - Empty Lines Ignored', function() { 182 $plugin = new TestableDeletePageGuard(); 183 $patterns = "^start$\n\n\n^sidebar$\n \n^end$"; 184 $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 185 $nonEmpty = array_filter($lines, function($line) { return trim($line) !== ''; }); 186 return count($nonEmpty) === 3; 187}); 188 189$runner->addTest('Configuration Parsing - Windows Line Endings', function() { 190 $plugin = new TestableDeletePageGuard(); 191 $patterns = "^start$\r\n^sidebar$\r\n^end$"; 192 $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 193 return count($lines) === 3; 194}); 195 196// Test 6: Security Features 197$runner->addTest('Security - Forward Slash Escaping', function() { 198 $plugin = new TestableDeletePageGuard(); 199 // Pattern with forward slashes should be properly escaped 200 return $plugin->matchesPattern('path/to/file', 'path/to/file') === true; 201}); 202 203$runner->addTest('Security - Unicode Support', function() { 204 $plugin = new TestableDeletePageGuard(); 205 // Test unicode pattern matching 206 return $plugin->matchesPattern('^café$', 'café') === true; 207}); 208 209$runner->addTest('Security - Special Regex Characters', function() { 210 $plugin = new TestableDeletePageGuard(); 211 // Test that dots are treated as literal dots when escaped 212 return $plugin->matchesPattern('file\.txt$', 'file.txt') === true; 213}); 214 215$runner->addTest('Security - Injection Protection', function() { 216 $plugin = new TestableDeletePageGuard(); 217 // Ensure patterns with potential injection attempts are handled safely 218 $result = $plugin->validateRegexPattern('(?{`ls`})'); 219 return is_string($result); // Should fail validation, not execute code 220}); 221 222// Test 7: Edge Cases 223$runner->addTest('Edge Cases - Empty Pattern', function() { 224 $plugin = new TestableDeletePageGuard(); 225 $result = $plugin->validateRegexPattern(''); 226 // Empty patterns should be considered invalid 227 return is_string($result) || $plugin->matchesPattern('', 'anything') === false; 228}); 229 230$runner->addTest('Edge Cases - Empty Target', function() { 231 $plugin = new TestableDeletePageGuard(); 232 return $plugin->matchesPattern('^$', '') === true; 233}); 234 235$runner->addTest('Edge Cases - Whitespace Pattern', function() { 236 $plugin = new TestableDeletePageGuard(); 237 return $plugin->matchesPattern('^\s+$', ' ') === true; 238}); 239 240$runner->addTest('Edge Cases - Very Long Target', function() { 241 $plugin = new TestableDeletePageGuard(); 242 $longTarget = str_repeat('a', 10000); 243 return $plugin->matchesPattern('^a+$', $longTarget) === true; 244}); 245 246// Test 8: Real-world Patterns 247$runner->addTest('Real-world - User Page Protection', function() { 248 $plugin = new TestableDeletePageGuard(); 249 $pattern = '^users:[^:]+:start$'; 250 return $plugin->matchesPattern($pattern, 'users:john:start') === true && 251 $plugin->matchesPattern($pattern, 'users:mary:start') === true && 252 $plugin->matchesPattern($pattern, 'users:admin:profile') === false; 253}); 254 255$runner->addTest('Real-world - Namespace Protection', function() { 256 $plugin = new TestableDeletePageGuard(); 257 $pattern = '^admin:.*$'; 258 return $plugin->matchesPattern($pattern, 'admin:config') === true && 259 $plugin->matchesPattern($pattern, 'admin:users:list') === true && 260 $plugin->matchesPattern($pattern, 'public:page') === false; 261}); 262 263$runner->addTest('Real-world - File Extension Pattern', function() { 264 $plugin = new TestableDeletePageGuard(); 265 $pattern = '\.txt$'; 266 return $plugin->matchesPattern($pattern, 'document.txt') === true && 267 $plugin->matchesPattern($pattern, 'image.png') === false; 268}); 269 270// Run all tests 271$runner->run();