History

Clean MCP per-user auth test boundaries

Rhys Sullivan committed 6 months ago

Commit 7656b2e1 parent b039b6e1 file · +18 −23Browse files

Unified · Split

packages/plugins/mcp/src/sdk/per-user-auth-isolation.test.ts+18−23
@@ -19,7 +19,7 @@ · 18 unmodified lines
1919 import * as http from "node:http";
2020
2121 import { describe, expect, it } from "@effect/vitest";
22−import { Cause, Effect, Exit } from "effect";
22+import { Cause, Effect, Exit, Predicate } from "effect";
2323 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2424 import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2525 import { z } from "zod";
@@ -37,6 +37,7 @@ import { · 11 unmodified lines
3737 definePlugin,
3838 makeInMemoryBlobStore,
3939 type SecretProvider,
40+ type ToolInvocationError,
4041 } from "@executor-js/sdk";
4142 import { makeMemoryAdapter } from "@executor-js/storage-core/testing/memory";
4243
@@ -96,6 +97,12 @@ type TestServer = { · 53 unmodified lines
9697 readonly recorded: () => readonly RecordedRequest[];
9798 };
9899
100+const failureError = <E>(exit: Exit.Exit<unknown, E>): E | undefined =>
101+ Exit.isFailure(exit) ? exit.cause.reasons.find(Cause.isFailReason)?.error : undefined;
102+
103+const isToolInvocationError = (error: unknown): error is ToolInvocationError =>
104+ Predicate.isTagged(error, "ToolInvocationError");
105+
99106 const createAuthRecordingServer: Effect.Effect<TestServer, Error, never> =
100107 Effect.callback<TestServer, Error>((resume) => {
101108 const transports = new Map<string, StreamableHTTPServerTransport>();
@@ -258,7 +265,7 @@ describe("per-user MCP auth isolation", () => { · 156 unmodified lines
258265 // stack [userB, org] can see them via fall-through.
259266 yield* execUserA.mcp.addSource({
260267 transport: "remote",
261− scope: ORG as string,
268+ scope: ORG,
262269 name: "Shared MCP",
263270 endpoint: server.url,
264271 namespace: "iso_test",
@@ -312,21 +319,15 @@ describe("per-user MCP auth isolation", () => { · 47 unmodified lines
312319 // Pin the exact error tag so a future regression that swaps
313320 // the "connection not found" check for a silent `auth: { kind:
314321 // "none" }` fallback would fail here, not silently connect.
315− if (!Exit.isFailure(userBResult)) return;
316322 // tools.invoke wraps plugin failures in ToolInvocationError
317323 // with the original error carried on `cause`. Pin the exact
318324 // inner tag — a regression that swapped the "no connection
319325 // found" check for a silent no-auth fallback would either
320326 // succeed outright (leaking) or surface a different tag here.
321− const failure = userBResult.cause.reasons.find(Cause.isFailReason);
322− const outer = failure?.error as
323− | {
324− _tag?: string;
325− cause?: { _tag?: string };
326− }
327− | undefined;
328− expect(outer?._tag).toBe("ToolInvocationError");
329− expect(outer?.cause?._tag).toBe("McpConnectionError");
327+ const outer = failureError(userBResult);
328+ expect(isToolInvocationError(outer)).toBe(true);
329+ const inner = isToolInvocationError(outer) ? outer.cause : undefined;
330+ expect(Predicate.isTagged(inner, "McpConnectionError")).toBe(true);
330331
331332 // CRITICAL: no outbound MCP request was made on user B's behalf
332333 // carrying user A's bearer token. Auth resolution must have
@@ -366,7 +367,7 @@ describe("per-user MCP auth isolation", () => { · 33 unmodified lines
366367
367368 yield* execUserA.mcp.addSource({
368369 transport: "remote",
369− scope: ORG as string,
370+ scope: ORG,
370371 name: "Shared MCP (header)",
371372 endpoint: server.url,
372373 namespace: "iso_header",
@@ -418,21 +419,15 @@ describe("per-user MCP auth isolation", () => { · 45 unmodified lines
418419 );
419420
420421 expect(Exit.isFailure(userBResult)).toBe(true);
421− if (!Exit.isFailure(userBResult)) return;
422422 // tools.invoke wraps plugin failures in ToolInvocationError
423423 // with the original error carried on `cause`. Pin the exact
424424 // inner tag — a regression that swapped the "no connection
425425 // found" check for a silent no-auth fallback would either
426426 // succeed outright (leaking) or surface a different tag here.
427− const failure = userBResult.cause.reasons.find(Cause.isFailReason);
428− const outer = failure?.error as
429− | {
430− _tag?: string;
431− cause?: { _tag?: string };
432− }
433− | undefined;
434− expect(outer?._tag).toBe("ToolInvocationError");
435− expect(outer?.cause?._tag).toBe("McpConnectionError");
427+ const outer = failureError(userBResult);
428+ expect(isToolInvocationError(outer)).toBe(true);
429+ const inner = isToolInvocationError(outer) ? outer.cause : undefined;
430+ expect(Predicate.isTagged(inner, "McpConnectionError")).toBe(true);
436431
437432 const afterUserB = server.recorded().slice(recordedBeforeUserB);
438433 for (const req of afterUserB) {