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