2026-01-29 19:48:36 +09:00
|
|
|
import { describe, it, expect, spyOn, afterEach } from "bun:test"
|
|
|
|
|
import * as mcpOauth from "./mcp-oauth"
|
|
|
|
|
|
|
|
|
|
describe("mcp-oauth check", () => {
|
|
|
|
|
describe("getMcpOAuthCheckDefinition", () => {
|
|
|
|
|
it("returns check definition with correct properties", () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
|
|
|
|
// when getting definition
|
2026-01-29 19:48:36 +09:00
|
|
|
const def = mcpOauth.getMcpOAuthCheckDefinition()
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then should have correct structure
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(def.id).toBe("mcp-oauth-tokens")
|
|
|
|
|
expect(def.name).toBe("MCP OAuth Tokens")
|
|
|
|
|
expect(def.category).toBe("tools")
|
|
|
|
|
expect(def.critical).toBe(false)
|
|
|
|
|
expect(typeof def.check).toBe("function")
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("checkMcpOAuthTokens", () => {
|
|
|
|
|
let readStoreSpy: ReturnType<typeof spyOn>
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
readStoreSpy?.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns skip when no tokens stored", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given no OAuth tokens configured
|
2026-01-29 19:48:36 +09:00
|
|
|
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue(null)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when checking OAuth tokens
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = await mcpOauth.checkMcpOAuthTokens()
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then should skip
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result.status).toBe("skip")
|
|
|
|
|
expect(result.message).toContain("No OAuth")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns pass when all tokens valid", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given valid tokens with future expiry (expiresAt is in epoch seconds)
|
2026-01-29 19:48:36 +09:00
|
|
|
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
|
|
|
|
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
|
|
|
|
"example.com/resource1": {
|
|
|
|
|
accessToken: "token1",
|
|
|
|
|
expiresAt: futureTime,
|
|
|
|
|
},
|
|
|
|
|
"example.com/resource2": {
|
|
|
|
|
accessToken: "token2",
|
|
|
|
|
expiresAt: futureTime,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when checking OAuth tokens
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = await mcpOauth.checkMcpOAuthTokens()
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then should pass
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result.status).toBe("pass")
|
|
|
|
|
expect(result.message).toContain("2")
|
|
|
|
|
expect(result.message).toContain("valid")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns warn when some tokens expired", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given mix of valid and expired tokens (expiresAt is in epoch seconds)
|
2026-01-29 19:48:36 +09:00
|
|
|
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
|
|
|
|
const pastTime = Math.floor(Date.now() / 1000) - 3600
|
|
|
|
|
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
|
|
|
|
"example.com/resource1": {
|
|
|
|
|
accessToken: "token1",
|
|
|
|
|
expiresAt: futureTime,
|
|
|
|
|
},
|
|
|
|
|
"example.com/resource2": {
|
|
|
|
|
accessToken: "token2",
|
|
|
|
|
expiresAt: pastTime,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when checking OAuth tokens
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = await mcpOauth.checkMcpOAuthTokens()
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then should warn
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result.status).toBe("warn")
|
|
|
|
|
expect(result.message).toContain("1")
|
|
|
|
|
expect(result.message).toContain("expired")
|
|
|
|
|
expect(result.details?.some((d: string) => d.includes("Expired"))).toBe(
|
|
|
|
|
true
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns pass when tokens have no expiry", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given tokens without expiry info
|
2026-01-29 19:48:36 +09:00
|
|
|
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
|
|
|
|
"example.com/resource1": {
|
|
|
|
|
accessToken: "token1",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when checking OAuth tokens
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = await mcpOauth.checkMcpOAuthTokens()
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then should pass (no expiry = assume valid)
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result.status).toBe("pass")
|
|
|
|
|
expect(result.message).toContain("1")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("includes token details in output", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given multiple tokens
|
2026-01-29 19:48:36 +09:00
|
|
|
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
|
|
|
|
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
|
|
|
|
"api.example.com/v1": {
|
|
|
|
|
accessToken: "token1",
|
|
|
|
|
expiresAt: futureTime,
|
|
|
|
|
},
|
|
|
|
|
"auth.example.com/oauth": {
|
|
|
|
|
accessToken: "token2",
|
|
|
|
|
expiresAt: futureTime,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when checking OAuth tokens
|
2026-01-29 19:48:36 +09:00
|
|
|
const result = await mcpOauth.checkMcpOAuthTokens()
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then should list tokens in details
|
2026-01-29 19:48:36 +09:00
|
|
|
expect(result.details).toBeDefined()
|
|
|
|
|
expect(result.details?.length).toBeGreaterThan(0)
|
|
|
|
|
expect(
|
|
|
|
|
result.details?.some((d: string) => d.includes("api.example.com"))
|
|
|
|
|
).toBe(true)
|
|
|
|
|
expect(
|
|
|
|
|
result.details?.some((d: string) => d.includes("auth.example.com"))
|
|
|
|
|
).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|