Files

77 lines
4.4 KiB
JavaScript

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);
}
});