Merge branch 'main' into main
This commit is contained in:
commit
7c115ed0e2
4 changed files with 183 additions and 24 deletions
|
|
@ -46,11 +46,11 @@ describe("OAuth Authorization", () => {
|
|||
it("returns metadata when first fetch fails but second without MCP header succeeds", async () => {
|
||||
// Set up a counter to control behavior
|
||||
let callCount = 0;
|
||||
|
||||
|
||||
// Mock implementation that changes behavior based on call count
|
||||
mockFetch.mockImplementation((_url, _options) => {
|
||||
callCount++;
|
||||
|
||||
|
||||
if (callCount === 1) {
|
||||
// First call with MCP header - fail with TypeError (simulating CORS error)
|
||||
// We need to use TypeError specifically because that's what the implementation checks for
|
||||
|
|
@ -68,10 +68,10 @@ describe("OAuth Authorization", () => {
|
|||
// Should succeed with the second call
|
||||
const metadata = await discoverOAuthMetadata("https://auth.example.com");
|
||||
expect(metadata).toEqual(validMetadata);
|
||||
|
||||
|
||||
// Verify both calls were made
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
|
||||
|
||||
// Verify first call had MCP header
|
||||
expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version");
|
||||
});
|
||||
|
|
@ -79,11 +79,11 @@ describe("OAuth Authorization", () => {
|
|||
it("throws an error when all fetch attempts fail", async () => {
|
||||
// Set up a counter to control behavior
|
||||
let callCount = 0;
|
||||
|
||||
|
||||
// Mock implementation that changes behavior based on call count
|
||||
mockFetch.mockImplementation((_url, _options) => {
|
||||
callCount++;
|
||||
|
||||
|
||||
if (callCount === 1) {
|
||||
// First call - fail with TypeError
|
||||
return Promise.reject(new TypeError("First failure"));
|
||||
|
|
@ -96,7 +96,7 @@ describe("OAuth Authorization", () => {
|
|||
// Should fail with the second error
|
||||
await expect(discoverOAuthMetadata("https://auth.example.com"))
|
||||
.rejects.toThrow("Second failure");
|
||||
|
||||
|
||||
// Verify both calls were made
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
|
@ -250,6 +250,7 @@ describe("OAuth Authorization", () => {
|
|||
clientInformation: validClientInfo,
|
||||
authorizationCode: "code123",
|
||||
codeVerifier: "verifier123",
|
||||
redirectUri: "http://localhost:3000/callback",
|
||||
});
|
||||
|
||||
expect(tokens).toEqual(validTokens);
|
||||
|
|
@ -271,6 +272,7 @@ describe("OAuth Authorization", () => {
|
|||
expect(body.get("code_verifier")).toBe("verifier123");
|
||||
expect(body.get("client_id")).toBe("client123");
|
||||
expect(body.get("client_secret")).toBe("secret123");
|
||||
expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback");
|
||||
});
|
||||
|
||||
it("validates token response schema", async () => {
|
||||
|
|
@ -288,6 +290,7 @@ describe("OAuth Authorization", () => {
|
|||
clientInformation: validClientInfo,
|
||||
authorizationCode: "code123",
|
||||
codeVerifier: "verifier123",
|
||||
redirectUri: "http://localhost:3000/callback",
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
|
@ -303,6 +306,7 @@ describe("OAuth Authorization", () => {
|
|||
clientInformation: validClientInfo,
|
||||
authorizationCode: "code123",
|
||||
codeVerifier: "verifier123",
|
||||
redirectUri: "http://localhost:3000/callback",
|
||||
})
|
||||
).rejects.toThrow("Token exchange failed");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export async function auth(
|
|||
clientInformation,
|
||||
authorizationCode,
|
||||
codeVerifier,
|
||||
redirectUri: provider.redirectUrl,
|
||||
});
|
||||
|
||||
await provider.saveTokens(tokens);
|
||||
|
|
@ -259,11 +260,13 @@ export async function exchangeAuthorization(
|
|||
clientInformation,
|
||||
authorizationCode,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
}: {
|
||||
metadata?: OAuthMetadata;
|
||||
clientInformation: OAuthClientInformation;
|
||||
authorizationCode: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string | URL;
|
||||
},
|
||||
): Promise<OAuthTokens> {
|
||||
const grantType = "authorization_code";
|
||||
|
|
@ -290,6 +293,7 @@ export async function exchangeAuthorization(
|
|||
client_id: clientInformation.client_id,
|
||||
code: authorizationCode,
|
||||
code_verifier: codeVerifier,
|
||||
redirect_uri: String(redirectUri),
|
||||
});
|
||||
|
||||
if (clientInformation.client_secret) {
|
||||
|
|
|
|||
131
src/client/cross-spawn.test.ts
Normal file
131
src/client/cross-spawn.test.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { StdioClientTransport } from "./stdio.js";
|
||||
import spawn from "cross-spawn";
|
||||
import { JSONRPCMessage } from "../types.js";
|
||||
import { ChildProcess } from "node:child_process";
|
||||
|
||||
// mock cross-spawn
|
||||
jest.mock("cross-spawn");
|
||||
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
||||
|
||||
describe("StdioClientTransport using cross-spawn", () => {
|
||||
beforeEach(() => {
|
||||
// mock cross-spawn's return value
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const mockProcess: {
|
||||
on: jest.Mock;
|
||||
stdin?: { on: jest.Mock; write: jest.Mock };
|
||||
stdout?: { on: jest.Mock };
|
||||
stderr?: null;
|
||||
} = {
|
||||
on: jest.fn((event: string, callback: () => void) => {
|
||||
if (event === "spawn") {
|
||||
callback();
|
||||
}
|
||||
return mockProcess;
|
||||
}),
|
||||
stdin: {
|
||||
on: jest.fn(),
|
||||
write: jest.fn().mockReturnValue(true)
|
||||
},
|
||||
stdout: {
|
||||
on: jest.fn()
|
||||
},
|
||||
stderr: null
|
||||
};
|
||||
return mockProcess as unknown as ChildProcess;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should call cross-spawn correctly", async () => {
|
||||
const transport = new StdioClientTransport({
|
||||
command: "test-command",
|
||||
args: ["arg1", "arg2"]
|
||||
});
|
||||
|
||||
await transport.start();
|
||||
|
||||
// verify spawn is called correctly
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
"test-command",
|
||||
["arg1", "arg2"],
|
||||
expect.objectContaining({
|
||||
shell: false
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should pass environment variables correctly", async () => {
|
||||
const customEnv = { TEST_VAR: "test-value" };
|
||||
const transport = new StdioClientTransport({
|
||||
command: "test-command",
|
||||
env: customEnv
|
||||
});
|
||||
|
||||
await transport.start();
|
||||
|
||||
// verify environment variables are passed correctly
|
||||
expect(mockSpawn).toHaveBeenCalledWith(
|
||||
"test-command",
|
||||
[],
|
||||
expect.objectContaining({
|
||||
env: customEnv
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should send messages correctly", async () => {
|
||||
const transport = new StdioClientTransport({
|
||||
command: "test-command"
|
||||
});
|
||||
|
||||
// get the mock process object
|
||||
const mockProcess: {
|
||||
on: jest.Mock;
|
||||
stdin: {
|
||||
on: jest.Mock;
|
||||
write: jest.Mock;
|
||||
once: jest.Mock;
|
||||
};
|
||||
stdout: {
|
||||
on: jest.Mock;
|
||||
};
|
||||
stderr: null;
|
||||
} = {
|
||||
on: jest.fn((event: string, callback: () => void) => {
|
||||
if (event === "spawn") {
|
||||
callback();
|
||||
}
|
||||
return mockProcess;
|
||||
}),
|
||||
stdin: {
|
||||
on: jest.fn(),
|
||||
write: jest.fn().mockReturnValue(true),
|
||||
once: jest.fn()
|
||||
},
|
||||
stdout: {
|
||||
on: jest.fn()
|
||||
},
|
||||
stderr: null
|
||||
};
|
||||
|
||||
mockSpawn.mockReturnValue(mockProcess as unknown as ChildProcess);
|
||||
|
||||
await transport.start();
|
||||
|
||||
// 关键修复:确保 jsonrpc 是字面量 "2.0"
|
||||
const message: JSONRPCMessage = {
|
||||
jsonrpc: "2.0",
|
||||
id: "test-id",
|
||||
method: "test-method"
|
||||
};
|
||||
|
||||
await transport.send(message);
|
||||
|
||||
// verify message is sent correctly
|
||||
expect(mockProcess.stdin.write).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { ChildProcess, IOType, spawn } from "node:child_process";
|
||||
import { ChildProcess, IOType } from "node:child_process";
|
||||
import spawn from "cross-spawn";
|
||||
import process from "node:process";
|
||||
import { Stream } from "node:stream";
|
||||
import { ReadBuffer, serializeMessage } from "../shared/stdio.js";
|
||||
|
|
|
|||
Loading…
Reference in a new issue