125 lines
5.0 KiB
JavaScript
125 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* DC-058 share preview page static-analysis test.
|
|
*
|
|
* Validates the static contract of status/share/index.html:
|
|
* 1. parses cleanly (no malformed HTML/CSS/JS)
|
|
* 2. exposes the expected public endpoints (preview fetch + subscribe POST)
|
|
* 3. does NOT call the redeem-tailscale endpoint from the client (the
|
|
* redemption flow lives on Caddy, not the browser — see the
|
|
* server-side handler at routes/share.js)
|
|
* 4. extracts the share token from the URL path
|
|
* 5. shows the right CTA copy for public vs Tailscale shares
|
|
*
|
|
* This is a regression guard for the "fake Tailscale redemption" bug codex
|
|
* flagged in the first review pass: an earlier version of the page POSTed
|
|
* a random deviceId to /redeem-tailscale, which silently consumed the
|
|
* one-shot share and broke the legitimate Tailscale join.
|
|
*
|
|
* Source path resolution: the standard location is `status/share/index.html`.
|
|
* The judge-artifact.sh wrapper sometimes copies the file into a flat
|
|
* worktree with a numeric prefix (e.g. `3_index.html`), so we fall back
|
|
* to a directory scan.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
function findTarget(name) {
|
|
const candidates = [
|
|
path.join(__dirname, '..', 'share', 'index.html'),
|
|
path.join(__dirname, 'share', 'index.html'),
|
|
path.join(__dirname, 'index.html'),
|
|
];
|
|
for (const p of candidates) {
|
|
try { if (fs.statSync(p).isFile()) return p; } catch (_) { /* keep looking */ }
|
|
}
|
|
const dir = __dirname;
|
|
let entries = [];
|
|
try { entries = fs.readdirSync(dir); } catch (_) { return null; }
|
|
const match = entries.find(e => e === name || e.endsWith('_' + name) || e.endsWith('-' + name));
|
|
return match ? path.join(dir, match) : null;
|
|
}
|
|
|
|
const SHARE_PAGE_PATH = findTarget('index.html');
|
|
if (!SHARE_PAGE_PATH) {
|
|
throw new Error(
|
|
'Cannot find share/index.html. Searched standard paths + directory scan of ' +
|
|
__dirname + '. If running under judge-artifact.sh, ensure the wrapper ' +
|
|
'passed the file via --files.'
|
|
);
|
|
}
|
|
|
|
let pageHtml;
|
|
let pageSource;
|
|
function loadPage() {
|
|
pageHtml = fs.readFileSync(SHARE_PAGE_PATH, 'utf8');
|
|
const scriptMatch = pageHtml.match(/<script>([\s\S]*?)<\/script>/);
|
|
pageSource = scriptMatch ? scriptMatch[1] : '';
|
|
return { html: pageHtml, source: pageSource };
|
|
}
|
|
|
|
test('share preview page exists and is non-empty', () => {
|
|
const { html } = loadPage();
|
|
assert.ok(html.length > 1000, 'expected non-trivial HTML');
|
|
assert.match(html, /<title>DashCaddy Share<\/title>/);
|
|
});
|
|
|
|
test('share preview page inline JS parses without syntax errors', () => {
|
|
const { source } = loadPage();
|
|
assert.doesNotThrow(() => new vm.Script(source, { filename: 'share-preview.js' }));
|
|
});
|
|
|
|
test('share preview page calls the preview endpoint relative to the token', () => {
|
|
const { source } = loadPage();
|
|
assert.match(source, /\/api\/v1\/share\/.*\/preview/,
|
|
'page must fetch the share preview via GET /api/v1/share/<token>/preview');
|
|
// CRITICAL: the redemption endpoint must NEVER be called from the client.
|
|
// The Tailscale join is a server-side flow (Caddy forward_auth checks the
|
|
// share store on each request — the link itself is the credential).
|
|
assert.doesNotMatch(source, /\/redeem-tailscale/,
|
|
'page must NOT call /redeem-tailscale — redemption is server-side');
|
|
});
|
|
|
|
test('share preview page calls the subscribe endpoint, not the issue endpoint', () => {
|
|
const { source } = loadPage();
|
|
assert.match(source, /\/api\/v1\/share\/.*\/subscribe/,
|
|
'page must allow subscribing via POST /api/v1/share/<token>/subscribe');
|
|
});
|
|
|
|
test('share preview page extracts the token from the URL path', () => {
|
|
const { source } = loadPage();
|
|
assert.match(source, /window\.location\.pathname/,
|
|
'page must read the share token from the URL path');
|
|
assert.match(source, /split\(['"]\/['"]\)/,
|
|
'page must split the path on "/" to extract the token');
|
|
});
|
|
|
|
test('share preview page has both public and Tailscale CTAs', () => {
|
|
const { html, source } = loadPage();
|
|
assert.match(html, /id="cta-public"/);
|
|
assert.match(html, /id="cta-tailscale"/);
|
|
assert.match(html, /tailscale\.com\/download/);
|
|
assert.match(source, /data\.kind === 'tailscale'/,
|
|
'script must branch the CTA on the kind= field returned by the API');
|
|
});
|
|
|
|
test('share preview page full source passes new Function() syntax check', () => {
|
|
const { source } = loadPage();
|
|
assert.doesNotThrow(() => new Function(source));
|
|
});
|
|
|
|
test('share preview page does NOT mention a fake / pending deviceId', () => {
|
|
// Regression guard for the original bug: the page used to fabricate a
|
|
// random "pending-XXXXXX" deviceId and POST it to /redeem-tailscale,
|
|
// which silently consumed the one-shot share. The page now has no
|
|
// client-side redemption path.
|
|
const { source } = loadPage();
|
|
assert.doesNotMatch(source, /pending-/, 'no placeholder deviceId fabrication');
|
|
assert.doesNotMatch(source, /Math\.random/, 'no random fallback that used to invent deviceIds');
|
|
});
|