import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { test } from "bun:test"; import { createCloudApiServer } from "./server.ts"; test("preset user login keeps YAML nav claims when OpenFGA tuple sync is unavailable", async () => { const password = "preset-user-password"; const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_ACCESS_CONTROL_USERS_JSON: JSON.stringify([ { id: "usr_v03_huiggao", username: "huiggao@163.com", displayName: "huiggao@163.com", email: "huiggao@163.com", role: "user", status: "active", navProfile: "code-mdtodo-only", passwordHashEnv: "HWLAB_PRESET_USER_HUIGGAO_PASSWORD_HASH" } ]), HWLAB_ACCESS_CONTROL_NAV_PROFILES_JSON: JSON.stringify([ { id: "code-mdtodo-only", allowedIds: ["workbench.code", "project.mdtodo"] } ]), HWLAB_PRESET_USER_HUIGGAO_PASSWORD_HASH: stablePasswordHash(password) }, openFgaAuthorizer: throwingOpenFgaAuthorizer() }); await listen(server); try { const { port } = server.address() as { port: number }; const login = await fetch(`http://127.0.0.1:${port}/auth/login`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ username: "huiggao@163.com", password }) }); assert.equal(login.status, 200); const loginBody = await login.json(); assert.deepEqual(loginBody.access.nav.allowedIds, ["workbench.code", "project.mdtodo"]); const cookie = login.headers.get("set-cookie")?.split(";")[0] ?? ""; assert.match(cookie, /^hwlab_session=/u); const session = await fetch(`http://127.0.0.1:${port}/auth/session`, { headers: { cookie } }); assert.equal(session.status, 200); const sessionBody = await session.json(); assert.equal(sessionBody.actor.username, "huiggao@163.com"); assert.deepEqual(sessionBody.access.nav.allowedIds, ["workbench.code", "project.mdtodo"]); } finally { await close(server); } }); function stablePasswordHash(password: string) { const salt = "preset-user-test-salt"; return `sha256:${salt}:${createHash("sha256").update(`${salt}:${password}`).digest("hex")}`; } function throwingOpenFgaAuthorizer() { const error = Object.assign(new Error("openfga unavailable"), { code: "openfga_unavailable" }); return { async describe() { return { ok: false, code: "openfga_unavailable" }; }, async check() { throw error; }, async writeTuple() { throw error; }, async deleteTuple() { throw error; } }; } function listen(server: any) { return new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); } function close(server: any) { return new Promise((resolve, reject) => server.close((error: Error | undefined) => (error ? reject(error) : resolve()))); }