包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。 排除 node_modules、npm 缓存、sessions 等运行时数据。
374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
/**
|
|
* Tests for mcp-auth.ts - Auth storage module
|
|
*/
|
|
|
|
import { describe, it, before, after } from "node:test"
|
|
import assert from "node:assert"
|
|
import { mkdirSync, rmSync, existsSync } from "fs"
|
|
import { join } from "path"
|
|
import { tmpdir } from "os"
|
|
import { randomBytes } from "crypto"
|
|
|
|
// Set up isolated temp directory for tests
|
|
const TEST_DIR = join(tmpdir(), `mcp-oauth-test-${randomBytes(4).toString('hex')}`)
|
|
process.env.MCP_OAUTH_DIR = TEST_DIR
|
|
|
|
import {
|
|
getAuthEntry,
|
|
getAuthForUrl,
|
|
saveAuthEntry,
|
|
removeAuthEntry,
|
|
updateTokens,
|
|
updateClientInfo,
|
|
updateCodeVerifier,
|
|
clearCodeVerifier,
|
|
updateOAuthState,
|
|
getOAuthState,
|
|
clearOAuthState,
|
|
isTokenExpired,
|
|
hasStoredTokens,
|
|
clearAllCredentials,
|
|
clearClientInfo,
|
|
clearTokens,
|
|
type AuthEntry,
|
|
} from "./mcp-auth.ts"
|
|
|
|
describe("mcp-auth", () => {
|
|
before(() => {
|
|
// Ensure clean state
|
|
try {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true, force: true })
|
|
}
|
|
mkdirSync(TEST_DIR, { recursive: true })
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
})
|
|
|
|
after(() => {
|
|
// Clean up temp directory
|
|
try {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true, force: true })
|
|
}
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
})
|
|
|
|
describe("getAuthEntry", () => {
|
|
it("should return undefined for non-existent entry", () => {
|
|
const entry = getAuthEntry("non-existent")
|
|
assert.strictEqual(entry, undefined)
|
|
})
|
|
})
|
|
|
|
describe("saveAuthEntry / getAuthEntry", () => {
|
|
it("should save and retrieve an auth entry", () => {
|
|
const entry: AuthEntry = {
|
|
tokens: {
|
|
accessToken: "test-token",
|
|
refreshToken: "refresh-token",
|
|
expiresAt: 1234567890,
|
|
scope: "read write",
|
|
},
|
|
serverUrl: "https://api.example.com",
|
|
}
|
|
|
|
saveAuthEntry("test-server", entry, "https://api.example.com")
|
|
const retrieved = getAuthEntry("test-server")
|
|
|
|
assert.deepStrictEqual(retrieved, entry)
|
|
})
|
|
|
|
it("should update existing entries", () => {
|
|
const entry1: AuthEntry = {
|
|
tokens: { accessToken: "token1" },
|
|
serverUrl: "https://api.example.com",
|
|
}
|
|
const entry2: AuthEntry = {
|
|
tokens: { accessToken: "token2" },
|
|
serverUrl: "https://api.example.com",
|
|
}
|
|
|
|
saveAuthEntry("test-server", entry1, "https://api.example.com")
|
|
saveAuthEntry("test-server", entry2, "https://api.example.com")
|
|
const retrieved = getAuthEntry("test-server")
|
|
|
|
assert.strictEqual(retrieved?.tokens?.accessToken, "token2")
|
|
})
|
|
})
|
|
|
|
describe("getAuthForUrl", () => {
|
|
it("should return entry when URL matches", () => {
|
|
const entry: AuthEntry = {
|
|
tokens: { accessToken: "test-token" },
|
|
serverUrl: "https://api.example.com",
|
|
}
|
|
|
|
saveAuthEntry("test-server", entry, "https://api.example.com")
|
|
const retrieved = getAuthForUrl("test-server", "https://api.example.com")
|
|
|
|
assert.deepStrictEqual(retrieved, entry)
|
|
})
|
|
|
|
it("should return undefined when URL doesn't match", () => {
|
|
const entry: AuthEntry = {
|
|
tokens: { accessToken: "test-token" },
|
|
serverUrl: "https://api.example.com",
|
|
}
|
|
|
|
saveAuthEntry("test-server", entry, "https://api.example.com")
|
|
const retrieved = getAuthForUrl("test-server", "https://different.com")
|
|
|
|
assert.strictEqual(retrieved, undefined)
|
|
})
|
|
|
|
it("should return undefined when serverUrl is not stored", () => {
|
|
const entry: AuthEntry = {
|
|
tokens: { accessToken: "test-token" },
|
|
}
|
|
|
|
saveAuthEntry("test-server", entry)
|
|
const retrieved = getAuthForUrl("test-server", "https://api.example.com")
|
|
|
|
assert.strictEqual(retrieved, undefined)
|
|
})
|
|
})
|
|
|
|
describe("removeAuthEntry", () => {
|
|
it("should remove an entry", () => {
|
|
const entry: AuthEntry = {
|
|
tokens: { accessToken: "test-token" },
|
|
}
|
|
|
|
saveAuthEntry("test-server", entry)
|
|
removeAuthEntry("test-server")
|
|
const retrieved = getAuthEntry("test-server")
|
|
|
|
assert.strictEqual(retrieved, undefined)
|
|
})
|
|
})
|
|
|
|
describe("updateTokens", () => {
|
|
it("should update tokens for a server", () => {
|
|
updateTokens("test-server", {
|
|
accessToken: "new-token",
|
|
refreshToken: "new-refresh",
|
|
expiresAt: 1234567890,
|
|
scope: "read",
|
|
})
|
|
|
|
const entry = getAuthEntry("test-server")
|
|
assert.strictEqual(entry?.tokens?.accessToken, "new-token")
|
|
})
|
|
|
|
it("should preserve existing client info", () => {
|
|
updateClientInfo("test-server", { clientId: "client-123" })
|
|
updateTokens("test-server", { accessToken: "token" })
|
|
|
|
const entry = getAuthEntry("test-server")
|
|
assert.strictEqual(entry?.clientInfo?.clientId, "client-123")
|
|
assert.strictEqual(entry?.tokens?.accessToken, "token")
|
|
})
|
|
|
|
it("should clear URL-bound auth state when tokens move to a different server URL", () => {
|
|
saveAuthEntry("token-url-change", {
|
|
tokens: { accessToken: "old-token", refreshToken: "old-refresh" },
|
|
clientInfo: { clientId: "old-client" },
|
|
codeVerifier: "old-verifier",
|
|
oauthState: "old-state",
|
|
serverUrl: "https://old.example.com/mcp",
|
|
}, "https://old.example.com/mcp")
|
|
|
|
updateTokens("token-url-change", { accessToken: "new-token" }, "https://new.example.com/mcp")
|
|
|
|
assert.strictEqual(getAuthForUrl("token-url-change", "https://old.example.com/mcp"), undefined)
|
|
const newEntry = getAuthForUrl("token-url-change", "https://new.example.com/mcp")
|
|
assert.strictEqual(newEntry?.tokens?.accessToken, "new-token")
|
|
assert.strictEqual(newEntry?.clientInfo, undefined)
|
|
assert.strictEqual(newEntry?.codeVerifier, undefined)
|
|
assert.strictEqual(newEntry?.oauthState, undefined)
|
|
})
|
|
|
|
it("should clear legacy URL-bound auth state when saving tokens with a server URL", () => {
|
|
saveAuthEntry("token-legacy-url-change", {
|
|
tokens: { accessToken: "old-token", refreshToken: "old-refresh" },
|
|
clientInfo: { clientId: "old-client" },
|
|
codeVerifier: "old-verifier",
|
|
oauthState: "old-state",
|
|
})
|
|
|
|
updateTokens("token-legacy-url-change", { accessToken: "new-token" }, "https://new.example.com/mcp")
|
|
|
|
const newEntry = getAuthForUrl("token-legacy-url-change", "https://new.example.com/mcp")
|
|
assert.strictEqual(newEntry?.tokens?.accessToken, "new-token")
|
|
assert.strictEqual(newEntry?.clientInfo, undefined)
|
|
assert.strictEqual(newEntry?.codeVerifier, undefined)
|
|
assert.strictEqual(newEntry?.oauthState, undefined)
|
|
})
|
|
})
|
|
|
|
describe("updateClientInfo", () => {
|
|
it("should update client info for a server", () => {
|
|
updateClientInfo("test-server", {
|
|
clientId: "client-123",
|
|
clientSecret: "secret",
|
|
clientIdIssuedAt: 1234567890,
|
|
clientSecretExpiresAt: 1234567999,
|
|
})
|
|
|
|
const entry = getAuthEntry("test-server")
|
|
assert.strictEqual(entry?.clientInfo?.clientId, "client-123")
|
|
assert.strictEqual(entry?.clientInfo?.clientSecret, "secret")
|
|
})
|
|
|
|
it("should clear URL-bound credentials when client info moves to a different server URL", () => {
|
|
saveAuthEntry("url-change", {
|
|
tokens: { accessToken: "old-token", refreshToken: "old-refresh" },
|
|
clientInfo: { clientId: "old-client" },
|
|
codeVerifier: "old-verifier",
|
|
oauthState: "old-state",
|
|
serverUrl: "https://old.example.com/mcp",
|
|
}, "https://old.example.com/mcp")
|
|
|
|
updateClientInfo("url-change", { clientId: "new-client" }, "https://new.example.com/mcp")
|
|
|
|
assert.strictEqual(getAuthForUrl("url-change", "https://old.example.com/mcp"), undefined)
|
|
const newEntry = getAuthForUrl("url-change", "https://new.example.com/mcp")
|
|
assert.strictEqual(newEntry?.clientInfo?.clientId, "new-client")
|
|
assert.strictEqual(newEntry?.tokens, undefined)
|
|
assert.strictEqual(newEntry?.codeVerifier, undefined)
|
|
assert.strictEqual(newEntry?.oauthState, undefined)
|
|
})
|
|
|
|
it("should clear stale verifier and state when legacy client info gains a server URL", () => {
|
|
saveAuthEntry("legacy-url-change", {
|
|
tokens: { accessToken: "old-token", refreshToken: "old-refresh" },
|
|
clientInfo: { clientId: "old-client" },
|
|
codeVerifier: "old-verifier",
|
|
oauthState: "old-state",
|
|
})
|
|
|
|
updateClientInfo("legacy-url-change", { clientId: "new-client" }, "https://new.example.com/mcp")
|
|
|
|
const newEntry = getAuthForUrl("legacy-url-change", "https://new.example.com/mcp")
|
|
assert.strictEqual(newEntry?.clientInfo?.clientId, "new-client")
|
|
assert.strictEqual(newEntry?.tokens, undefined)
|
|
assert.strictEqual(newEntry?.codeVerifier, undefined)
|
|
assert.strictEqual(newEntry?.oauthState, undefined)
|
|
})
|
|
})
|
|
|
|
describe("updateCodeVerifier / clearCodeVerifier", () => {
|
|
it("should save and retrieve code verifier", () => {
|
|
updateCodeVerifier("test-server", "verifier-123")
|
|
const entry = getAuthEntry("test-server")
|
|
assert.strictEqual(entry?.codeVerifier, "verifier-123")
|
|
})
|
|
|
|
it("should clear code verifier", () => {
|
|
updateCodeVerifier("test-server", "verifier-123")
|
|
clearCodeVerifier("test-server")
|
|
const entry = getAuthEntry("test-server")
|
|
assert.strictEqual(entry?.codeVerifier, undefined)
|
|
})
|
|
})
|
|
|
|
describe("updateOAuthState / getOAuthState / clearOAuthState", () => {
|
|
it("should save and retrieve OAuth state", () => {
|
|
updateOAuthState("test-server", "state-abc-123")
|
|
const state = getOAuthState("test-server")
|
|
assert.strictEqual(state, "state-abc-123")
|
|
})
|
|
|
|
it("should clear OAuth state", () => {
|
|
updateOAuthState("test-server", "state-abc-123")
|
|
clearOAuthState("test-server")
|
|
const state = getOAuthState("test-server")
|
|
assert.strictEqual(state, undefined)
|
|
})
|
|
})
|
|
|
|
describe("isTokenExpired", () => {
|
|
it("should return null if no tokens", () => {
|
|
const expired = isTokenExpired("expiry-test-null")
|
|
assert.strictEqual(expired, null)
|
|
})
|
|
|
|
it("should return false if no expiry", () => {
|
|
updateTokens("expiry-test-no-expiry", { accessToken: "token" })
|
|
const expired = isTokenExpired("expiry-test-no-expiry")
|
|
assert.strictEqual(expired, false)
|
|
})
|
|
|
|
it("should return true if expired", () => {
|
|
updateTokens("expiry-test-expired", {
|
|
accessToken: "token",
|
|
expiresAt: 1, // Way in the past
|
|
})
|
|
const expired = isTokenExpired("expiry-test-expired")
|
|
assert.strictEqual(expired, true)
|
|
})
|
|
|
|
it("should return false if not expired", () => {
|
|
updateTokens("expiry-test-future", {
|
|
accessToken: "token",
|
|
expiresAt: Date.now() / 1000 + 3600, // 1 hour from now
|
|
})
|
|
const expired = isTokenExpired("expiry-test-future")
|
|
assert.strictEqual(expired, false)
|
|
})
|
|
})
|
|
|
|
describe("hasStoredTokens", () => {
|
|
it("should return false if no tokens", () => {
|
|
assert.strictEqual(hasStoredTokens("has-tokens-test-false"), false)
|
|
})
|
|
|
|
it("should return true if tokens exist", () => {
|
|
updateTokens("has-tokens-test-true", { accessToken: "token" })
|
|
assert.strictEqual(hasStoredTokens("has-tokens-test-true"), true)
|
|
})
|
|
})
|
|
|
|
describe("clearAllCredentials", () => {
|
|
it("should remove all credentials", () => {
|
|
updateTokens("test-server", { accessToken: "token" })
|
|
updateClientInfo("test-server", { clientId: "client" })
|
|
updateCodeVerifier("test-server", "verifier")
|
|
|
|
clearAllCredentials("test-server")
|
|
|
|
assert.strictEqual(getAuthEntry("test-server"), undefined)
|
|
})
|
|
})
|
|
|
|
describe("clearClientInfo", () => {
|
|
it("should only remove client info", () => {
|
|
updateTokens("test-server", { accessToken: "token" })
|
|
updateClientInfo("test-server", { clientId: "client" })
|
|
|
|
clearClientInfo("test-server")
|
|
|
|
const entry = getAuthEntry("test-server")
|
|
assert.strictEqual(entry?.clientInfo, undefined)
|
|
assert.strictEqual(entry?.tokens?.accessToken, "token")
|
|
})
|
|
})
|
|
|
|
describe("clearTokens", () => {
|
|
it("should only remove tokens", () => {
|
|
updateTokens("test-server", { accessToken: "token" })
|
|
updateClientInfo("test-server", { clientId: "client" })
|
|
|
|
clearTokens("test-server")
|
|
|
|
const entry = getAuthEntry("test-server")
|
|
assert.strictEqual(entry?.tokens, undefined)
|
|
assert.strictEqual(entry?.clientInfo?.clientId, "client")
|
|
})
|
|
})
|
|
})
|