import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import test from "node:test"; import vm from "node:vm"; // Use the workspace's shared frontend compiler, without loading the application. const require = createRequire(new URL("../../../govoplan-core/webui/package.json", import.meta.url)); const { transformSync } = require("esbuild"); const calls = []; function load(relativePath, imports = {}) { const source = readFileSync(new URL(relativePath, import.meta.url), "utf8"); const code = transformSync(source, { loader: "ts", format: "cjs", target: "es2022" }).code; const context = vm.createContext({ module: { exports: {} }, require: () => imports }); context.exports = context.module.exports; vm.runInContext(code, context); return context.module.exports; } const api = load("../src/api/passwords.ts", { apiFetch: (...args) => { calls.push(args); return Promise.resolve({}); }, isApiError: (error) => Boolean(error?.fixtureApiError) }); const { passwordTranslations } = load("../src/i18n/passwordTranslations.ts"); const settings = { apiBaseUrl: "https://fixture.invalid", apiKey: "fixture-key", accessToken: "legacy-fixture-token" }; const current = "fixture-current-password"; const next = "fixture-next-password"; const code = "fixture-recovery-code"; test("password requests carry credentials only in POST bodies and public calls discard bearer settings", async () => { calls.length = 0; await api.fetchPasswordPolicy(settings); await api.changePassword(settings, current, next); await api.issuePasswordRecovery(settings, "account/1", current, true); await api.recoverPassword(settings, "person@example.test", code, next); assert.equal(calls[0][0].apiKey, ""); assert.equal(calls[0][0].accessToken, ""); assert.equal(calls[0][2].cache, "no-store"); assert.equal(calls[1][0], settings); assert.deepEqual(JSON.parse(calls[1][2].body), { current_password: current, new_password: next }); assert.equal(calls[2][1], "/api/v1/auth/password/recovery/account%2F1"); assert.deepEqual(JSON.parse(calls[2][2].body), { current_password: current, identity_verified: true }); assert.equal(calls[3][0].apiKey, ""); assert.equal(calls[3][0].accessToken, ""); assert.deepEqual(JSON.parse(calls[3][2].body), { email: "person@example.test", recovery_code: code, new_password: next }); for (const [, path, options] of calls.slice(1)) { assert.equal(options.method, "POST"); for (const secret of [current, next, code]) assert.equal(path.includes(secret), false); } }); test("all stable password errors have EN/DE messages, while arbitrary input never becomes display text", () => { const codes = ["current_password_invalid", "invalid_new_password", "password_unchanged", "password_recovery_disabled", "password_rate_limited", "local_password_unavailable", "recovery_invalid", "recovery_issuer_required", "recovery_membership_required", "password_changed_concurrently", "password_change_required"]; for (const value of codes) { const key = api.passwordErrorMessage({ fixtureApiError: true, status: 400, body: JSON.stringify({ detail: { code: value, input: current } }) }); assert.ok(passwordTranslations.en[key], value); assert.ok(passwordTranslations.de[key], value); assert.notEqual(key, "i18n:govoplan-access.password.request_failed"); } for (const body of [current, JSON.stringify({ detail: [{ input: current }] }), JSON.stringify({ detail: { code: "toString", input: code } })]) { assert.equal(api.passwordErrorMessage({ fixtureApiError: true, status: 500, body }), "i18n:govoplan-access.password.request_failed"); } assert.equal(api.passwordErrorMessage(new Error(current)), "i18n:govoplan-access.password.request_failed"); for (const status of [401, 403, 422, 429]) { const key = api.passwordErrorMessage({ fixtureApiError: true, status, body: JSON.stringify({ detail: current }) }); assert.ok(passwordTranslations.en[key]); assert.ok(passwordTranslations.de[key]); assert.equal(key.includes(current), false); } }); test("password translations keep the EN/DE workflow and placeholder contracts aligned", () => { assert.deepEqual(Object.keys(passwordTranslations.en).sort(), Object.keys(passwordTranslations.de).sort()); for (const key of Object.keys(passwordTranslations.en)) { assert.deepEqual(passwordTranslations.en[key].match(/\{value\d+\}/g) ?? [], passwordTranslations.de[key].match(/\{value\d+\}/g) ?? [], key); } });