This repository has been archived on 2026-06-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
a2a-mcp/tests/oauth.test.ts
Robert 2814112ab3
All checks were successful
build / test (push) Successful in 19s
publish / npm (push) Successful in 22s
Use Keycloak OAuth for a2amcp login
2026-05-31 20:46:17 -03:00

96 lines
3.2 KiB
TypeScript

import { test, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { promises as fs } from "node:fs";
import { loadCredentials, saveCredentials } from "../src/credentials.js";
import {
buildAuthorizationUrl,
createPkcePair,
refreshCredentialsIfNeeded,
} from "../src/oauth.js";
let originalHome: string | undefined;
let tmpHome: string;
const originalFetch = globalThis.fetch;
beforeEach(async () => {
originalHome = process.env.HOME;
tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "a2a-mcp-oauth-"));
process.env.HOME = tmpHome;
globalThis.fetch = originalFetch;
});
afterEach(async () => {
if (originalHome !== undefined) process.env.HOME = originalHome;
else delete process.env.HOME;
globalThis.fetch = originalFetch;
await fs.rm(tmpHome, { recursive: true, force: true });
});
test("PKCE authorization URL requests Keycloak code flow", () => {
const pkce = createPkcePair();
assert.ok(pkce.verifier.length > 40);
assert.ok(pkce.challenge.length > 40);
const url = new URL(
buildAuthorizationUrl({
authorizationEndpoint: "https://auth.example.test/auth",
clientId: "a2acloud-cli",
redirectUri: "http://127.0.0.1:41873/callback",
scope: "openid email mcp:invoke",
state: "state-123",
codeChallenge: pkce.challenge,
}),
);
assert.equal(url.searchParams.get("client_id"), "a2acloud-cli");
assert.equal(url.searchParams.get("response_type"), "code");
assert.equal(url.searchParams.get("code_challenge_method"), "S256");
assert.equal(url.searchParams.get("scope"), "openid email mcp:invoke");
});
test("refreshCredentialsIfNeeded refreshes expired Keycloak access token", async () => {
const calls: Array<{ url: string; body?: string }> = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
calls.push({ url, body: init?.body?.toString() });
if (url.endsWith("/.well-known/openid-configuration")) {
return new Response(
JSON.stringify({
authorization_endpoint: "https://auth.example.test/auth",
token_endpoint: "https://auth.example.test/token",
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
}
return new Response(
JSON.stringify({
access_token: "fresh-token",
refresh_token: "fresh-refresh",
expires_in: 3600,
scope: "openid email mcp:invoke",
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
}) as typeof fetch;
await saveCredentials({
apiUrl: "https://api.example.test",
token: "old-token",
refreshToken: "refresh-token",
expiresAt: 1,
email: "dev@example.test",
issuer: "https://auth.example.test",
clientId: "a2acloud-cli",
});
const refreshed = await refreshCredentialsIfNeeded((await loadCredentials())!);
assert.equal(refreshed.token, "fresh-token");
assert.equal(refreshed.refreshToken, "fresh-refresh");
assert.equal((await loadCredentials())!.token, "fresh-token");
assert.ok(calls.some((call) => call.body?.includes("grant_type=refresh_token")));
});