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