Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage.

This commit is contained in:
Bob Dickinson 2026-07-04 21:56:45 -07:00
commit 3c65bdb4c6

View file

@ -1,7 +1,8 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
NodeOAuthStorage,
getOAuthStore,
getStateFilePath,
resetNodeOAuthStorageCache,
clearAllOAuthClientState,
} from "@inspector/core/auth/node/storage-node.js";
import type {
@ -20,44 +21,27 @@ const testStatePath = path.join(
`mcp-inspector-oauth-${process.pid}-storage-node.json`,
);
async function resetTestStorage(stateFilePath: string): Promise<void> {
try {
await fs.unlink(stateFilePath);
} catch {
// Ignore if file doesn't exist
}
resetNodeOAuthStorageCache(stateFilePath);
}
describe("NodeOAuthStorage", () => {
let storage: NodeOAuthStorage;
const testServerUrl = "http://localhost:3000";
const stateFilePath = testStatePath;
beforeEach(async () => {
// Clean up any existing state file
try {
await fs.unlink(stateFilePath);
} catch {
// Ignore if file doesn't exist
}
// Reset store state by clearing all servers
const store = getOAuthStore(testStatePath);
const state = store.getState();
// Clear all server states
Object.keys(state.servers).forEach((url) => {
state.clearServerState(url);
});
await resetTestStorage(stateFilePath);
storage = new NodeOAuthStorage(testStatePath);
});
afterEach(async () => {
// Clean up state file after each test
try {
await fs.unlink(stateFilePath);
} catch {
// Ignore if file doesn't exist
}
// Reset store state
const store = getOAuthStore(testStatePath);
const state = store.getState();
Object.keys(state.servers).forEach((url) => {
state.clearServerState(url);
});
await resetTestStorage(stateFilePath);
});
describe("getClientInformation", () => {
@ -88,11 +72,10 @@ describe("NodeOAuthStorage", () => {
client_secret: "preregistered-secret",
};
// Store as preregistered by directly setting it in the store
const store = getOAuthStore(testStatePath);
store.getState().setServerState(testServerUrl, {
preregisteredClientInformation: preregisteredInfo,
});
await storage.savePreregisteredClientInformation(
testServerUrl,
preregisteredInfo,
);
const result = await storage.getClientInformation(testServerUrl, true);
@ -341,16 +324,16 @@ describe("NodeOAuthStorage", () => {
it("clearCodeVerifier removes only the PKCE verifier", async () => {
await storage.saveCodeVerifier(testServerUrl, "verifier");
expect(storage.getCodeVerifier(testServerUrl)).toBe("verifier");
expect(await storage.getCodeVerifier(testServerUrl)).toBe("verifier");
await storage.clearCodeVerifier(testServerUrl);
expect(storage.getCodeVerifier(testServerUrl)).toBeUndefined();
expect(await storage.getCodeVerifier(testServerUrl)).toBeUndefined();
});
it("clearScope removes only the scope", async () => {
await storage.saveScope(testServerUrl, "read write");
expect(storage.getScope(testServerUrl)).toBe("read write");
expect(await storage.getScope(testServerUrl)).toBe("read write");
await storage.clearScope(testServerUrl);
expect(storage.getScope(testServerUrl)).toBeUndefined();
expect(await storage.getScope(testServerUrl)).toBeUndefined();
});
it("clearServerMetadata removes only the cached metadata", async () => {
@ -361,9 +344,9 @@ describe("NodeOAuthStorage", () => {
response_types_supported: ["code"],
};
await storage.saveServerMetadata(testServerUrl, metadata);
expect(storage.getServerMetadata(testServerUrl)).toEqual(metadata);
expect(await storage.getServerMetadata(testServerUrl)).toEqual(metadata);
await storage.clearServerMetadata(testServerUrl);
expect(storage.getServerMetadata(testServerUrl)).toBeNull();
expect(await storage.getServerMetadata(testServerUrl)).toBeNull();
});
});
@ -437,83 +420,52 @@ describe("NodeOAuthStorage", () => {
expect(result2).toEqual(clientInfo2);
});
});
});
describe("OAuth Store (Zustand)", () => {
const stateFilePath = testStatePath;
it("reuses in-memory state for the same file path", async () => {
const serverUrl = "http://localhost:3000";
const tokens: OAuthTokens = {
access_token: "shared-memory-token",
token_type: "Bearer",
};
beforeEach(async () => {
try {
await fs.unlink(stateFilePath);
} catch {
// Ignore if file doesn't exist
}
await storage.saveTokens(serverUrl, tokens);
const otherView = new NodeOAuthStorage(testStatePath);
expect(await otherView.getTokens(serverUrl)).toEqual(tokens);
});
afterEach(async () => {
try {
await fs.unlink(stateFilePath);
} catch {
// Ignore if file doesn't exist
}
});
it("should create a new store", () => {
const store = getOAuthStore(testStatePath);
expect(store).toBeDefined();
expect(store.getState).toBeDefined();
expect(store.setState).toBeDefined();
});
it("should return the same store instance via getOAuthStore", () => {
const store1 = getOAuthStore(testStatePath);
const store2 = getOAuthStore(testStatePath);
expect(store1).toBe(store2);
});
it("should persist state to file", async () => {
// Use a unique path so no other test (e.g. "should overwrite..." with second-id) can
// write to the same file; Zustand persist is async and can race with shared paths.
it("persists state to file on save", async () => {
const persistTestPath = path.join(
os.tmpdir(),
`mcp-inspector-oauth-persist-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
);
try {
if (process.env.DEBUG_PERSIST_PATH === "1") {
console.error("[storage-node.test] state file path:", persistTestPath);
}
const store = getOAuthStore(persistTestPath);
resetNodeOAuthStorageCache(persistTestPath);
const fileStorage = new NodeOAuthStorage(persistTestPath);
const serverUrl = "http://localhost:3000";
const clientInfo: OAuthClientInformation = {
client_id: "test-client-id",
};
store.getState().setServerState(serverUrl, {
clientInformation: clientInfo,
await fileStorage.saveClientInformation(serverUrl, clientInfo, {
registrationKind: "dcr",
});
type StateShape = {
state: {
servers: Record<
string,
{ clientInformation?: OAuthClientInformation }
>;
};
servers: Record<string, { clientInformation?: OAuthClientInformation }>;
};
// Persistence is fire-and-forget; await the write rather than polling.
await flushStoreFileWrites(persistTestPath);
const parsed = JSON.parse(
await fs.readFile(persistTestPath, "utf-8"),
) as StateShape;
expect(parsed.state.servers[serverUrl]?.clientInformation).toEqual(
clientInfo,
);
expect(parsed.servers[serverUrl]?.clientInformation).toEqual(clientInfo);
} finally {
try {
await fs.unlink(persistTestPath);
} catch {
/* ignore */
}
resetNodeOAuthStorageCache(persistTestPath);
}
});
});
@ -572,9 +524,7 @@ describe("NodeOAuthStorage with custom storagePath", () => {
await storage.saveTokens(testServerUrl, tokens);
type StateShape = {
state: {
servers: Record<string, { tokens?: { access_token?: string } }>;
};
servers: Record<string, { tokens?: { access_token?: string } }>;
};
// Persistence is fire-and-forget; await the write rather than polling.
await flushStoreFileWrites(customPath);
@ -582,7 +532,7 @@ describe("NodeOAuthStorage with custom storagePath", () => {
await fs.readFile(customPath, "utf-8"),
) as StateShape;
expect(parsed.state.servers[testServerUrl]?.tokens?.access_token).toBe(
expect(parsed.servers[testServerUrl]?.tokens?.access_token).toBe(
tokens.access_token,
);
@ -599,33 +549,34 @@ describe("NodeOAuthStorage with custom storagePath", () => {
});
it("clearAllOAuthClientState clears every server in the default store", async () => {
const defaultStore = getOAuthStore();
defaultStore.getState().setServerState("http://server-a.test", {
tokens: { access_token: "a", token_type: "Bearer" },
resetNodeOAuthStorageCache();
const storage = new NodeOAuthStorage();
await storage.saveTokens("http://server-a.test", {
access_token: "a",
token_type: "Bearer",
});
defaultStore.getState().setServerState("http://server-b.test", {
tokens: { access_token: "b", token_type: "Bearer" },
await storage.saveTokens("http://server-b.test", {
access_token: "b",
token_type: "Bearer",
});
clearAllOAuthClientState();
const all = defaultStore.getState();
expect(all.getServerState("http://server-a.test").tokens).toBeUndefined();
expect(all.getServerState("http://server-b.test").tokens).toBeUndefined();
await flushStoreFileWrites(getStateFilePath());
await clearAllOAuthClientState();
expect(await storage.getTokens("http://server-a.test")).toBeUndefined();
expect(await storage.getTokens("http://server-b.test")).toBeUndefined();
resetNodeOAuthStorageCache();
});
it("clearAllOAuthClientState tolerates a store whose servers map is absent", () => {
// Exercises the `state.servers ?? {}` nullish fallback: when the persisted
// state lacks a `servers` map, the iteration must default to {} (no throw).
const defaultStore = getOAuthStore();
const clearServerState = vi.fn();
vi.spyOn(defaultStore, "getState").mockReturnValue({
servers: undefined,
clearServerState,
} as unknown as ReturnType<typeof defaultStore.getState>);
it("clearAllOAuthClientState tolerates persisted state without a servers map", async () => {
const filePath = getStateFilePath();
resetNodeOAuthStorageCache();
await fs.writeFile(filePath, JSON.stringify({ idpSessions: {} }), "utf-8");
expect(() => clearAllOAuthClientState()).not.toThrow();
expect(clearServerState).not.toHaveBeenCalled();
await expect(clearAllOAuthClientState()).resolves.toBeUndefined();
vi.mocked(defaultStore.getState).mockRestore();
resetNodeOAuthStorageCache();
});
it("should isolate state from default store", async () => {
@ -635,12 +586,14 @@ describe("NodeOAuthStorage with custom storagePath", () => {
);
try {
resetNodeOAuthStorageCache();
const defaultStorage = new NodeOAuthStorage();
await defaultStorage.saveTokens(testServerUrl, {
access_token: "default-token",
token_type: "Bearer",
});
resetNodeOAuthStorageCache(customPath);
const customStorage = new NodeOAuthStorage(customPath);
await customStorage.saveTokens(testServerUrl, {
access_token: "custom-token",
@ -653,13 +606,15 @@ describe("NodeOAuthStorage with custom storagePath", () => {
const fromDefault = await defaultStorage.getTokens(testServerUrl);
expect(fromDefault?.access_token).toBe("default-token");
getOAuthStore().getState().clearServerState(testServerUrl);
await defaultStorage.clear(testServerUrl);
} finally {
try {
await fs.unlink(customPath);
} catch {
/* ignore */
}
resetNodeOAuthStorageCache(customPath);
resetNodeOAuthStorageCache();
}
});
});