110 lines
5.3 KiB
JavaScript
110 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ObsiGate — Account security section non-regression tests (BUG-068).
|
|
*
|
|
* Static checks on the "🔒 Sécurité du compte" configuration section:
|
|
* - BUG-068a: the TOTP QR code must NOT depend on the third-party
|
|
* https://api.qrserver.com service (blocked by the CSP
|
|
* `img-src 'self' data: blob:`, so the QR never displayed — and the
|
|
* otpauth URI, TOTP secret included, leaked to a third party). The setup
|
|
* endpoint returns a local SVG data: URI (qr_data_url) instead.
|
|
* - BUG-068b: .config-btn-primary / .config-btn-danger are used by
|
|
* frontend/js/auth.js but were never defined — buttons fell back to the
|
|
* browser default and ignored the theme. They must exist and derive from
|
|
* theme variables.
|
|
* - BUG-068c: recovery codes issued on first-time WebAuthn enable were lost
|
|
* (no #mfa-setup-flow-area in the "already enabled" view).
|
|
* - BUG-068d: the section had no password change although
|
|
* POST /api/auth/change-password exists.
|
|
*
|
|
* Usage: node tests/frontend/mfa-settings.test.mjs
|
|
*/
|
|
|
|
import { strict as assert } from "node:assert";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
const auth = readFileSync(path.join(ROOT, "frontend", "js", "auth.js"), "utf8");
|
|
const css = readFileSync(path.join(ROOT, "frontend", "style.css"), "utf8");
|
|
const router = readFileSync(path.join(ROOT, "backend", "auth", "router.py"), "utf8");
|
|
const fr = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "fr.json"), "utf8"));
|
|
const en = JSON.parse(readFileSync(path.join(ROOT, "frontend", "locales", "en.json"), "utf8"));
|
|
|
|
function test(label, fn) {
|
|
try {
|
|
fn();
|
|
console.log(" ✓ " + label);
|
|
} catch (err) {
|
|
console.error(" ✗ " + label + "\n " + err.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
// ── QR code: local data URI, no third-party service ─────────────────────────
|
|
test("auth.js — no external QR service left (CSP blocks it, secret leaks)", () => {
|
|
assert.doesNotMatch(auth, /qrserver\.com/, "api.qrserver.com is blocked by img-src and leaks the otpauth URI");
|
|
assert.doesNotMatch(auth, /https:\/\/api\./, "no third-party https://api.* image may carry the TOTP secret");
|
|
});
|
|
|
|
test("auth.js — setup flow renders the backend qr_data_url with a fallback", () => {
|
|
assert.match(auth, /qr_data_url/, "the QR <img> must use the backend-provided qr_data_url");
|
|
assert.match(auth, /mfa-qr-fallback/, "a manual-entry fallback must show when no QR is available");
|
|
assert.match(auth, /mfa\.qr_unavailable/, "the fallback needs its i18n string");
|
|
});
|
|
|
|
test("backend — /mfa/totp/setup returns a local qr_data_url", () => {
|
|
assert.match(router, /qr_data_url/, "setup must include qr_data_url in its response");
|
|
assert.match(router, /svg_data_uri/, "the QR must be generated locally (segno SVG data URI)");
|
|
assert.match(router, /import segno/, "segno import must stay local with a graceful fallback");
|
|
});
|
|
|
|
// ── Buttons follow the theme ────────────────────────────────────────────────
|
|
for (const cls of ["config-btn-primary", "config-btn-danger"]) {
|
|
test(`style.css — .${cls} is defined from theme variables`, () => {
|
|
const rule = css.match(new RegExp(`\\.${cls}\\s*\\{([^}]*)\\}`));
|
|
assert.ok(rule, `.${cls} rule not found — buttons fall back to the browser default`);
|
|
assert.match(rule[1], /var\(--/, `.${cls} must derive from CSS theme variables, not hardcoded colors`);
|
|
});
|
|
}
|
|
|
|
test("style.css — themed buttons have disabled states", () => {
|
|
assert.match(css, /\.config-btn-primary:disabled/, ".config-btn-primary needs a disabled state");
|
|
assert.match(css, /\.config-btn-danger:disabled/, ".config-btn-danger needs a disabled state");
|
|
});
|
|
|
|
// ── Recovery codes are never lost ───────────────────────────────────────────
|
|
test("auth.js — _showRecoveryCodes falls back to the WebAuthn flow area", () => {
|
|
const fn = auth.match(/function _showRecoveryCodes\(codes(?:, targetId)?\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "_showRecoveryCodes helper not found");
|
|
assert.match(fn[1], /webauthn-flow-area/, "codes issued on first WebAuthn enable must render without #mfa-setup-flow-area");
|
|
});
|
|
|
|
// ── Password change lives in the security section ───────────────────────────
|
|
test("auth.js — password change calls POST /api/auth/change-password", () => {
|
|
assert.match(auth, /\/api\/auth\/change-password/, "changePassword must hit the existing endpoint");
|
|
assert.match(auth, /_renderPasswordSection/, "the security section must render a password card");
|
|
});
|
|
|
|
test("i18n — password + QR strings exist in FR and EN", () => {
|
|
for (const key of [
|
|
"mfa.qr_unavailable",
|
|
"mfa.password_change_title",
|
|
"mfa.password_change_btn",
|
|
"mfa.password_mismatch",
|
|
"mfa.password_changed",
|
|
]) {
|
|
assert.ok(fr[key], `fr.json missing ${key}`);
|
|
assert.ok(en[key], `en.json missing ${key}`);
|
|
}
|
|
});
|
|
|
|
if (process.exitCode) {
|
|
console.error("\nMFA settings tests FAILED");
|
|
} else {
|
|
console.log("\nAll MFA settings tests passed.");
|
|
}
|