fix: use Node storage in the revocation e2e, and drop the now-dead storage API (#2144)

The CI coverage job failed with `ReferenceError: sessionStorage is not defined`.
`revocation-e2e.test.ts` used `BrowserOAuthStorage`, but the `integration`
vitest project runs in the node environment, not happy-dom. Node 22 gates Web
Storage behind a flag while newer Node exposes it by default, so it passed
locally and failed in CI. It uses `NodeOAuthStorage` with a temp dir now,
matching the other OAuth integration tests, with a comment naming the trap.

Also from Copilot review round 21: `listIssuers` and `getIssuerTokens` became
dead production API when `takeRevocationSnapshot` took over — a grep finds them
only in their own implementations and tests. Keeping them would widen the
exported `OAuthStorage` contract and force every implementation and mock to
carry methods nothing calls. Removed, along with their tests and the mock
entries in four suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-28 08:36:58 -04:00
commit eede9c17ba

View file

@ -11,13 +11,21 @@
*/
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createHash, randomBytes } from "node:crypto";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
TestServerHttp,
createOAuthTestServerConfig,
getDefaultServerConfig,
waitForOAuthWellKnown,
} from "@modelcontextprotocol/inspector-test-server";
import { BrowserOAuthStorage } from "@inspector/core/auth/browser/storage.js";
// NOT `BrowserOAuthStorage`: the `integration` project runs in the node
// environment, where `sessionStorage` does not exist. Node 22 gates Web Storage
// behind a flag and newer Node exposes it by default, so a browser-backed store
// passes on a developer's machine and fails in CI — which is exactly what
// happened here.
import { NodeOAuthStorage } from "@inspector/core/auth/node/storage-node.js";
import {
clearAndPlanRevocation,
executeOAuthRevocation,
@ -43,6 +51,7 @@ function base64Url(buffer: Buffer): string {
describe("OAuth token revocation (RFC 7009)", () => {
let mcpServer: TestServerHttp | null = null;
let serverUrl = "";
let storageDir = "";
let metadata: OAuthMetadata;
beforeAll(async () => {
@ -67,6 +76,7 @@ describe("OAuth token revocation (RFC 7009)", () => {
});
const port = await mcpServer.start();
serverUrl = `http://localhost:${port}`;
storageDir = mkdtempSync(join(tmpdir(), "mcp-inspector-2144-"));
await waitForOAuthWellKnown(serverUrl);
metadata = (await (
await fetch(`${serverUrl}/.well-known/oauth-authorization-server`)
@ -76,6 +86,10 @@ describe("OAuth token revocation (RFC 7009)", () => {
afterAll(async () => {
await mcpServer?.stop();
mcpServer = null;
if (storageDir) {
rmSync(storageDir, { recursive: true, force: true });
storageDir = "";
}
}, 30_000);
/** Run a real authorization-code exchange and return the issued tokens. */
@ -135,8 +149,8 @@ describe("OAuth token revocation (RFC 7009)", () => {
async function seededStorage(tokens: {
access_token: string;
refresh_token?: string;
}): Promise<BrowserOAuthStorage> {
const storage = new BrowserOAuthStorage();
}): Promise<NodeOAuthStorage> {
const storage = new NodeOAuthStorage(join(storageDir, "oauth.json"));
await storage.clear(serverUrl);
// Issuer-bound and matching the discovered metadata: an unkeyed grant
// records no authorization server and is deliberately refused.
@ -207,7 +221,7 @@ describe("OAuth token revocation (RFC 7009)", () => {
* exercises the ordering the product uses rather than a convenience wrapper.
*/
async function clearAndRevoke(
storage: BrowserOAuthStorage,
storage: NodeOAuthStorage,
): Promise<TokenRevocationOutcome> {
const plan = await clearAndPlanRevocation({ serverUrl, storage });
return executeOAuthRevocation(plan, { fetchFn: fetch });