xref: /plugin/deletepageguard/tests/test_runner.php (revision c176b8b34e4ca9181ac383002f75ffe5441c68b3)
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();