aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/test-webdav-paths.mjs136
1 files changed, 136 insertions, 0 deletions
diff --git a/scripts/test-webdav-paths.mjs b/scripts/test-webdav-paths.mjs
new file mode 100644
index 0000000..a8340c0
--- /dev/null
+++ b/scripts/test-webdav-paths.mjs
@@ -0,0 +1,136 @@
+// v4.6.16 regression test: WebDAV remote-path normalization.
+// Reproduces the mindtech incident: AI passed full URLs / absolute
+// '/spaces/<user>/' paths which got blindly joined onto the config URL,
+// creating literal 'https:/' and nested 'spaces/<user>/' junk dirs.
+// Run: node scripts/test-webdav-paths.mjs (repo root = spaces/newos4)
+
+// Run: node scripts/test-webdav-paths.mjs (repo root = spaces/newos4)
+
+import { readFileSync } from 'fs';
+
+const src = readFileSync(new URL('../modules/ext/webdav.js', import.meta.url), 'utf8');
+
+class TerminalStub {
+ print() {}
+}
+
+// Mirrors ModuleManager.createModuleClass(): extract the first top-level class.
+const cleanCode = src.replace(/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*/, '');
+const classMatch = cleanCode.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/);
+if (!classMatch) throw new Error('no module class found in webdav.js');
+const WebDAVModule = new Function('window', 'localStorage', 'fetch', 'XMLHttpRequest', 'URL', 'crypto',
+ 'return ' + cleanCode + '\nreturn ' + classMatch[1])(
+ { location: { pathname: '/spaces/mindtech/newos4/main.html' } },
+ { getItem: () => null, setItem: () => {} },
+ async () => { throw new Error('network disabled in test'); },
+ function () { this.open = () => {}; this.setRequestHeader = () => {}; this.send = () => {}; },
+ URL, undefined);
+
+function makeMod() {
+ const m = new WebDAVModule(new TerminalStub());
+ m.config = { url: 'https://webx11.duckdns.org/spaces/mindtech/', username: 'mindtech', password: 'tok' };
+ return m;
+}
+
+let pass = 0, fail = 0;
+function check(name, cond, got) {
+ if (cond) { pass++; console.log(' ok - ' + name); }
+ else { fail++; console.log(' FAIL - ' + name + (got !== undefined ? ' got: ' + JSON.stringify(got) : '')); }
+}
+
+// --- _normalizeRemotePath ---
+{
+ const m = makeMod();
+ const cases = [
+ // [input, expected]
+ [null, '/'],
+ ['', '/'],
+ ['file.txt', '/file.txt'],
+ ['/file.txt', '/file.txt'],
+ // the mindtech incident shapes:
+ ['https://webx11.duckdns.org/spaces/mindtech/thaimassage-kiefersfelden/index-traditional.html',
+ '/thaimassage-kiefersfelden/index-traditional.html'],
+ ['/spaces/mindtech/thaimassage-kiefersfelden/index-professional.html',
+ '/thaimassage-kiefersfelden/index-professional.html'],
+ ['https://webx11.duckdns.org/spaces/mindtech', '/'],
+ ['spaces/mindtech/skills/navigating-the-terminal.json', '/skills/navigating-the-terminal.json'],
+ // relative + traversal + dupes:
+ ['./a//b/../c.txt', '/a/c.txt'],
+ ['/a/b/../../d.txt', '/d.txt'],
+ ['/skills/', '/skills'],
+ ['backups', '/backups'],
+ // foreign space stays intact (only own space prefix is stripped):
+ ['/spaces/otheruser/file.txt', '/spaces/otheruser/file.txt'],
+ ['https://evil.example.com/spaces/mindtech/x?query=1#frag', '/x'],
+ ];
+ for (const [input, expected] of cases) {
+ const got = m._normalizeRemotePath(input);
+ check(`normalize(${JSON.stringify(input)}) => ${expected}`, got === expected, got);
+ }
+}
+
+// --- _absoluteUrl (MOVE/COPY Destination) ---
+{
+ const m = makeMod();
+ const got = m._absoluteUrl('https://webx11.duckdns.org/spaces/mindtech/a/b.html');
+ check('_absoluteUrl strips to full absolute URI',
+ got === 'https://webx11.duckdns.org/spaces/mindtech/a/b.html', got);
+ const got2 = m._absoluteUrl('c.html');
+ check('_absoluteUrl builds from config base',
+ got2 === 'https://webx11.duckdns.org/spaces/mindtech/c.html', got2);
+}
+
+// --- _ensureDir normalizes before MKCOL walk ---
+{
+ const m = makeMod();
+ const mkcols = [];
+ m._webdavRequest = async (method, path) => {
+ if (method === 'MKCOL') mkcols.push(path);
+ return { ok: true, status: 201 };
+ };
+ m._listDir = async () => { throw new Error('ENOENT'); };
+ await m._ensureDir('https://webx11.duckdns.org/spaces/mindtech/thaimassage-kiefersfelden');
+ check('_ensureDir MKCOLs normalized path (no https:/ junk)',
+ mkcols.length === 1 && mkcols[0] === '/thaimassage-kiefersfelden', mkcols);
+
+ // Existing dir fast-path: __list__ probe succeeds -> no MKCOL at all
+ const m2 = makeMod();
+ const mkcols2 = [];
+ m2._listDir = async (p) => { if (p === '/skills') return []; throw new Error('ENOENT'); };
+ m2._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols2.push(path); return { ok: true, status: 201 }; };
+ await m2._ensureDir('/skills');
+ check('_ensureDir skips MKCOL when dir already exists', mkcols2.length === 0, mkcols2);
+
+ // Partial existing: /backups exists, /backups/2026 does not
+ const m3 = makeMod();
+ const mkcols3 = [];
+ m3._listDir = async (p) => { if (p === '/backups') return []; throw new Error('ENOENT'); };
+ m3._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols3.push(path); return { ok: true, status: 201 }; };
+ await m3._ensureDir('/backups/2026');
+ check('_ensureDir MKCOLs only missing tail', mkcols3.length === 1 && mkcols3[0] === '/backups/2026', mkcols3);
+}
+
+// --- PUT via _webdavRequest hits normalized URL ---
+{
+ // The class closes over the injected fetch param — rebuild with a
+ // recording fetch so we can assert the request URL.
+ let requested = null;
+ const recordFetch = async (url) => { requested = url; return { ok: true, status: 201 }; };
+ const WebDAVModule2 = new Function('window', 'localStorage', 'fetch', 'XMLHttpRequest', 'URL', 'crypto',
+ 'return ' + cleanCode + '\nreturn ' + classMatch[1])(
+ { location: { pathname: '/spaces/mindtech/newos4/main.html' } },
+ { getItem: () => null, setItem: () => {} },
+ recordFetch,
+ function () { this.open = () => {}; this.setRequestHeader = () => {}; this.send = () => {}; },
+ URL, undefined);
+ const m = new WebDAVModule2(new TerminalStub());
+ m.config = { url: 'https://webx11.duckdns.org/spaces/mindtech/', username: 'mindtech', password: 'tok' };
+ m._resolvePassword = async () => {};
+ m._getAuthHeader = () => ({});
+ await m._webdavRequest('PUT', 'https://webx11.duckdns.org/spaces/mindtech/site/index.html', { body: 'x' });
+ check('PUT goes to normalized URL',
+ requested === 'https://webx11.duckdns.org/spaces/mindtech/site/index.html', requested);
+}
+
+console.log(`\n${pass} passed, ${fail} failed`);
+process.exit(fail ? 1 : 0); \ No newline at end of file