fix MCP auth token refresh and disable legacy SSE
This commit is contained in:
141
mcp-server/test/sessionRefresh.test.js
Normal file
141
mcp-server/test/sessionRefresh.test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user