xref: /plugin/deletepageguard/tests/test_runner.php (revision 9a383d51b90310842e2a3f0f9d693178d0875b32)
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// Run all tests
283$runner->run();