import assert from "node:assert/strict"; import test from "node:test"; import { rebuildApiClient, runWithTokenRefreshRetry } from "../auth/apiClient.js"; import { mcpSessionCredentials, sessionApiClients, sessionCredentials } from "../auth/credentials.js"; import { refreshSessionCredentials } from "../auth/sessionRefresh.js"; const jwt = (expiresIn = 3600) => { const payload = Buffer.from(JSON.stringify({ exp: Math.floor(Date.now() / 1000) + expiresIn, })).toString("base64url"); return `header.${payload}.signature`; }; const credentials = (token) => ({ token, tokenHash: "hash", website: "demo.example", web_url: "https://demo.forge.example", api_web_url: "http://web:80", forge_host: "demo.forge.example", mode: "local", project_dir: "/opt/acai/webs/owner/demo", acai_user: "owner", mcp_secret: "acai_secret", role: "developer", }); test("deduplicates concurrent forced refreshes and updates session credentials", async () => { const sessionId = "refresh-session"; sessionCredentials.set(sessionId, credentials(jwt())); let calls = 0; const freshToken = jwt(7200); const fetcher = async () => { calls += 1; await new Promise((resolve) => setTimeout(resolve, 10)); return { success: true, token: freshToken, tokenHash: "fresh-hash", domain: "demo.example", web_url: "https://demo.forge.example", api_web_url: "http://web:80", project_dir: "/opt/acai/webs/owner/demo", }; }; const [first, second] = await Promise.all([ refreshSessionCredentials(sessionId, { force: true, fetcher }), refreshSessionCredentials(sessionId, { force: true, fetcher }), ]); assert.equal(calls, 1); assert.equal(first.token, freshToken); assert.equal(second.token, freshToken); assert.equal(sessionCredentials.get(sessionId).tokenHash, "fresh-hash"); assert.equal(mcpSessionCredentials.get(sessionId).credentials.mcp_secret, undefined); mcpSessionCredentials.delete(sessionId); sessionCredentials.delete(sessionId); }); test("cached Axios client reads the latest token for every request", async () => { const sessionId = "axios-session"; sessionCredentials.set(sessionId, credentials("old-token")); const client = await rebuildApiClient(sessionId); const observed = []; client.defaults.adapter = async (config) => { observed.push(config.headers["X-Acai-Token"]); return { data: {}, status: 200, statusText: "OK", headers: {}, config }; }; await client.get("/first"); sessionCredentials.set(sessionId, credentials("new-token")); await client.get("/second"); assert.deepEqual(observed, ["old-token", "new-token"]); sessionApiClients.delete(sessionId); sessionCredentials.delete(sessionId); }); test("403 token failure forces one refresh and retries the original request once", async () => { const sessionId = "retry-session"; sessionCredentials.set(sessionId, credentials("expired-token")); let refreshes = 0; let attempts = 0; const observed = []; const fresh = credentials("fresh-token"); const client = await rebuildApiClient(sessionId, { refreshCredentials: async (_sessionId, options) => { refreshes += 1; assert.equal(options.force, true); sessionCredentials.set(sessionId, fresh); return fresh; }, }); client.defaults.adapter = async (config) => { attempts += 1; observed.push(config.headers["X-Acai-Token"]); if (attempts === 1) { const error = new Error("Request failed with status code 403"); error.config = config; error.response = { status: 403, data: { error: "Token no valido" } }; throw error; } return { data: { success: true }, status: 200, statusText: "OK", headers: {}, config }; }; const response = await client.get("/retry-once"); assert.equal(response.data.success, true); assert.equal(refreshes, 1); assert.equal(attempts, 2); assert.deepEqual(observed, ["expired-token", "fresh-token"]); sessionApiClients.delete(sessionId); sessionCredentials.delete(sessionId); }); test("tool-level retry covers Axios clients outside the cached API client", async () => { let attempts = 0; let refreshes = 0; const operation = async () => { attempts += 1; if (attempts === 1) { const error = new Error("forbidden"); error.response = { status: 403, data: { error: "JWT expired" } }; throw error; } return "ok"; }; const result = await runWithTokenRefreshRetry("tool-session", operation, { refreshCredentials: async (_sessionId, options) => { refreshes += 1; assert.equal(options.force, true); }, }); assert.equal(result, "ok"); assert.equal(attempts, 2); assert.equal(refreshes, 1); });