test(mcp): migrate OAuth auto-connect tests (#27356)

This commit is contained in:
Kit Langton
2026-05-13 16:38:37 +00:00
committed by GitHub
parent f0635e365f
commit 533495ae20
@@ -1,5 +1,6 @@
import { test, expect, mock, beforeEach } from "bun:test" import { expect, mock, beforeEach } from "bun:test"
import { Effect } from "effect" import { Effect, Layer } from "effect"
import { testEffect } from "../lib/effect"
// Mock UnauthorizedError to match the SDK's class // Mock UnauthorizedError to match the SDK's class
class MockUnauthorizedError extends Error { class MockUnauthorizedError extends Error {
@@ -111,172 +112,125 @@ beforeEach(() => {
// Import modules after mocking // Import modules after mocking
const { MCP } = await import("../../src/mcp/index") const { MCP } = await import("../../src/mcp/index")
const { Instance } = await import("../../src/project/instance") const { Bus } = await import("../../src/bus")
const { WithInstance } = await import("../../src/project/with-instance") const { Config } = await import("../../src/config/config")
const { tmpdir } = await import("../fixture/fixture") const { McpAuth } = await import("../../src/mcp/auth")
const { McpOAuthProvider } = await import("../../src/mcp/oauth-provider")
const { AppFileSystem } = await import("@opencode-ai/core/filesystem")
const { CrossSpawnSpawner } = await import("@opencode-ai/core/cross-spawn-spawner")
test("first connect to OAuth server shows needs_auth instead of failed", async () => { const mcpTest = testEffect(
await using tmp = await tmpdir({ Layer.mergeAll(
init: async (dir) => { MCP.layer.pipe(
await Bun.write( Layer.provide(McpAuth.defaultLayer),
`${dir}/opencode.json`, Layer.provideMerge(Bus.layer),
JSON.stringify({ Layer.provide(Config.defaultLayer),
$schema: "https://opencode.ai/config.json", Layer.provide(CrossSpawnSpawner.defaultLayer),
mcp: { Layer.provide(AppFileSystem.defaultLayer),
"test-oauth": { ),
type: "remote", McpAuth.defaultLayer,
url: "https://example.com/mcp", ),
}, )
},
}), const config = (name: string) => ({
) mcp: {
[name]: {
type: "remote" as const,
url: "https://example.com/mcp",
}, },
}) },
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const result = await Effect.runPromise(
MCP.Service.use((mcp) =>
mcp.add("test-oauth", {
type: "remote",
url: "https://example.com/mcp",
}),
).pipe(Effect.provide(MCP.defaultLayer)),
)
const serverStatus = result.status as Record<string, { status: string; error?: string }>
// The server should be detected as needing auth, NOT as failed.
// Before the fix, provider.state() would throw a plain Error
// ("No OAuth state saved for MCP server: test-oauth") which was
// not caught as UnauthorizedError, causing status to be "failed".
expect(serverStatus["test-oauth"]).toBeDefined()
expect(serverStatus["test-oauth"].status).toBe("needs_auth")
},
})
}) })
test("state() generates a new state when none is saved", async () => { mcpTest.instance(
const { McpOAuthProvider } = await import("../../src/mcp/oauth-provider") "first connect to OAuth server shows needs_auth instead of failed",
const { McpAuth } = await import("../../src/mcp/auth") () =>
MCP.Service.use((mcp) =>
Effect.gen(function* () {
const result = yield* mcp.add("test-oauth", {
type: "remote",
url: "https://example.com/mcp",
})
await using tmp = await tmpdir() const serverStatus = result.status as Record<string, { status: string; error?: string }>
await WithInstance.provide({ // The server should be detected as needing auth, NOT as failed.
directory: tmp.path, // Before the fix, provider.state() would throw a plain Error
fn: async () => { // ("No OAuth state saved for MCP server: test-oauth") which was
const auth = await Effect.runPromise( // not caught as UnauthorizedError, causing status to be "failed".
Effect.gen(function* () { expect(serverStatus["test-oauth"]).toBeDefined()
return yield* McpAuth.Service expect(serverStatus["test-oauth"].status).toBe("needs_auth")
}).pipe(Effect.provide(McpAuth.defaultLayer)), }),
) ),
const provider = new McpOAuthProvider( { config: config("test-oauth") },
"test-state-gen", )
"https://example.com/mcp",
{},
{ onRedirect: async () => {} },
auth,
)
const entryBefore = await Effect.runPromise( mcpTest.instance("state() generates a new state when none is saved", () =>
McpAuth.Service.use((auth) => auth.get("test-state-gen")).pipe(Effect.provide(McpAuth.defaultLayer)), Effect.gen(function* () {
) const auth = yield* McpAuth.Service
expect(entryBefore?.oauthState).toBeUndefined() const provider = new McpOAuthProvider(
"test-state-gen",
"https://example.com/mcp",
{},
{ onRedirect: async () => {} },
auth,
)
// state() should generate and return a new state, not throw const entryBefore = yield* McpAuth.Service.use((auth) => auth.get("test-state-gen"))
const state = await provider.state() expect(entryBefore?.oauthState).toBeUndefined()
expect(typeof state).toBe("string")
expect(state.length).toBe(64) // 32 bytes as hex
// The generated state should be persisted // state() should generate and return a new state, not throw
const entryAfter = await Effect.runPromise( const state = yield* Effect.promise(() => provider.state())
McpAuth.Service.use((auth) => auth.get("test-state-gen")).pipe(Effect.provide(McpAuth.defaultLayer)), expect(typeof state).toBe("string")
) expect(state.length).toBe(64) // 32 bytes as hex
expect(entryAfter?.oauthState).toBe(state)
},
})
})
test("state() returns existing state when one is saved", async () => { // The generated state should be persisted
const { McpOAuthProvider } = await import("../../src/mcp/oauth-provider") const entryAfter = yield* McpAuth.Service.use((auth) => auth.get("test-state-gen"))
const { McpAuth } = await import("../../src/mcp/auth") expect(entryAfter?.oauthState).toBe(state)
}),
)
await using tmp = await tmpdir() mcpTest.instance("state() returns existing state when one is saved", () =>
Effect.gen(function* () {
const auth = yield* McpAuth.Service
const provider = new McpOAuthProvider(
"test-state-existing",
"https://example.com/mcp",
{},
{ onRedirect: async () => {} },
auth,
)
await WithInstance.provide({ // Pre-save a state
directory: tmp.path, const existingState = "pre-saved-state-value"
fn: async () => { yield* McpAuth.Service.use((auth) => auth.updateOAuthState("test-state-existing", existingState))
const auth = await Effect.runPromise(
Effect.gen(function* () {
return yield* McpAuth.Service
}).pipe(Effect.provide(McpAuth.defaultLayer)),
)
const provider = new McpOAuthProvider(
"test-state-existing",
"https://example.com/mcp",
{},
{ onRedirect: async () => {} },
auth,
)
// Pre-save a state // state() should return the existing state
const existingState = "pre-saved-state-value" const state = yield* Effect.promise(() => provider.state())
await Effect.runPromise( expect(state).toBe(existingState)
McpAuth.Service.use((auth) => auth.updateOAuthState("test-state-existing", existingState)).pipe( }),
Effect.provide(McpAuth.defaultLayer), )
),
)
// state() should return the existing state mcpTest.instance(
const state = await provider.state() "authenticate() stores a connected client when auth completes without redirect",
expect(state).toBe(existingState) () =>
}, MCP.Service.use((mcp) =>
}) Effect.gen(function* () {
}) const added = yield* mcp.add("test-oauth-connect", {
type: "remote",
url: "https://example.com/mcp",
})
const before = added.status as Record<string, { status: string; error?: string }>
expect(before["test-oauth-connect"]?.status).toBe("needs_auth")
test("authenticate() stores a connected client when auth completes without redirect", async () => { simulateAuthFlow = false
await using tmp = await tmpdir({ connectSucceedsImmediately = true
init: async (dir) => {
await Bun.write(
`${dir}/opencode.json`,
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
"test-oauth-connect": {
type: "remote",
url: "https://example.com/mcp",
},
},
}),
)
},
})
await WithInstance.provide({ const result = yield* mcp.authenticate("test-oauth-connect")
directory: tmp.path, expect(result.status).toBe("connected")
fn: async () => {
await Effect.runPromise(
MCP.Service.use((mcp) =>
Effect.gen(function* () {
const added = yield* mcp.add("test-oauth-connect", {
type: "remote",
url: "https://example.com/mcp",
})
const before = added.status as Record<string, { status: string; error?: string }>
expect(before["test-oauth-connect"]?.status).toBe("needs_auth")
simulateAuthFlow = false const after = yield* mcp.status()
connectSucceedsImmediately = true expect(after["test-oauth-connect"]?.status).toBe("connected")
}),
const result = yield* mcp.authenticate("test-oauth-connect") ),
expect(result.status).toBe("connected") { config: config("test-oauth-connect") },
)
const after = yield* mcp.status()
expect(after["test-oauth-connect"]?.status).toBe("connected")
}),
).pipe(Effect.provide(MCP.defaultLayer)),
)
},
})
})