This commit is contained in:
Minoru Mizutani 2025-04-29 16:18:35 +09:00
parent 615a6dead0
commit a77dd1f8e1
No known key found for this signature in database
9 changed files with 193 additions and 97 deletions

View file

@ -13,14 +13,18 @@ interface RequestLike {
*/ */
export class DenoHttpServer { export class DenoHttpServer {
private server: Deno.HttpServer | null = null; private server: Deno.HttpServer | null = null;
private routes: Map<string, (req: Request) => Promise<Response> | Response> = new Map(); private routes: Map<string, (req: Request) => Promise<Response> | Response> =
new Map();
/** /**
* Register a GET route handler * Register a GET route handler
* @param path The path to handle * @param path The path to handle
* @param handler The handler function * @param handler The handler function
*/ */
get(path: string, handler: (req: RequestLike, res: ResponseBuilder) => void): void { get(
path: string,
handler: (req: RequestLike, res: ResponseBuilder) => void,
): void {
this.routes.set(path, async (request: Request) => { this.routes.set(path, async (request: Request) => {
const url = new URL(request.url); const url = new URL(request.url);
const searchParams = url.searchParams; const searchParams = url.searchParams;
@ -51,12 +55,16 @@ export class DenoHttpServer {
* @param hostname Optional hostname to bind to * @param hostname Optional hostname to bind to
* @param callback Optional callback when server is ready * @param callback Optional callback when server is ready
*/ */
listen(port: number, hostname?: string | (() => void), callback?: () => void): Server { listen(
port: number,
hostname?: string | (() => void),
callback?: () => void,
): Server {
// Handle optional hostname parameter // Handle optional hostname parameter
let hostnameStr: string | undefined; let hostnameStr: string | undefined;
let callbackFn = callback; let callbackFn = callback;
if (typeof hostname === 'function') { if (typeof hostname === "function") {
callbackFn = hostname; callbackFn = hostname;
hostnameStr = undefined; hostnameStr = undefined;
} else { } else {
@ -79,7 +87,7 @@ export class DenoHttpServer {
// Route not found // Route not found
return new Response("Not Found", { status: 404 }); return new Response("Not Found", { status: 404 });
} },
}); });
// Return a dummy server object that mimics Node's HTTP server // Return a dummy server object that mimics Node's HTTP server

View file

@ -94,7 +94,9 @@ export default async function open(
if (!success) { if (!success) {
const errorDetails = new TextDecoder().decode(stderr).trim(); const errorDetails = new TextDecoder().decode(stderr).trim();
const stdoutDetails = new TextDecoder().decode(stdout).trim(); const stdoutDetails = new TextDecoder().decode(stdout).trim();
let errorMessage = `Failed to open "${target}". Command "${command} ${args.join(" ")}" exited with code ${code}.`; let errorMessage = `Failed to open "${target}". Command "${command} ${
args.join(" ")
}" exited with code ${code}.`;
if (errorDetails) errorMessage += `\nStderr: ${errorDetails}`; if (errorDetails) errorMessage += `\nStderr: ${errorDetails}`;
if (stdoutDetails) errorMessage += `\nStdout: ${stdoutDetails}`; // Include stdout too if (stdoutDetails) errorMessage += `\nStdout: ${stdoutDetails}`; // Include stdout too
throw new Error(errorMessage); throw new Error(errorMessage);
@ -109,10 +111,11 @@ export default async function open(
// xdg-open often returns immediately. Add a small delay as a basic wait. // xdg-open often returns immediately. Add a small delay as a basic wait.
await delay(1000); // Wait 1 second (adjust as necessary) await delay(1000); // Wait 1 second (adjust as necessary)
} }
} catch (error) { } catch (error) {
if (error instanceof Deno.errors.NotFound) { if (error instanceof Deno.errors.NotFound) {
throw new Error(`Failed to open "${target}": Command not found: ${command}`); throw new Error(
`Failed to open "${target}": Command not found: ${command}`,
);
} }
// Re-throw other errors or wrap them // Re-throw other errors or wrap them
throw error instanceof Error ? error : new Error(String(error)); throw error instanceof Error ? error : new Error(String(error));

View file

@ -20,17 +20,17 @@ export function log(str: string, ...rest: unknown[]) {
// Helper function to safely get a message identifier for logging // Helper function to safely get a message identifier for logging
function getMessageIdentifier(message: unknown): string | number | undefined { function getMessageIdentifier(message: unknown): string | number | undefined {
if (typeof message !== 'object' || message === null) return undefined; if (typeof message !== "object" || message === null) return undefined;
// Check if it's a request or notification with a method // Check if it's a request or notification with a method
if ('method' in message && message.method !== undefined) { if ("method" in message && message.method !== undefined) {
return String(message.method); return String(message.method);
} }
// Check if it's a response with an id // Check if it's a response with an id
if ('id' in message && message.id !== undefined) { if ("id" in message && message.id !== undefined) {
const id = message.id; const id = message.id;
return typeof id === 'string' || typeof id === 'number' ? id : undefined; return typeof id === "string" || typeof id === "number" ? id : undefined;
} }
return undefined; return undefined;
@ -295,8 +295,12 @@ export function findAvailablePort(
serverOrPort?: number | net.Server, serverOrPort?: number | net.Server,
): Promise<number> { ): Promise<number> {
// Handle if server parameter is a number (preferred port) // Handle if server parameter is a number (preferred port)
const preferredPort = typeof serverOrPort === "number" ? serverOrPort : undefined; const preferredPort = typeof serverOrPort === "number"
const serverToUse = typeof serverOrPort !== "number" ? (serverOrPort as net.Server) : net.createServer(); ? serverOrPort
: undefined;
const serverToUse = typeof serverOrPort !== "number"
? (serverOrPort as net.Server)
: net.createServer();
let hasResolved = false; let hasResolved = false;
// Maximum number of port attempts before giving up // Maximum number of port attempts before giving up

View file

@ -1,11 +1,9 @@
import { import { assertEquals } from "std/assert/mod.ts";
assertEquals, import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts";
} from "std/assert/mod.ts";
import { describe, it, afterEach, beforeEach } from "std/testing/bdd.ts";
import { stub } from "std/testing/mock.ts"; import { stub } from "std/testing/mock.ts";
import { import {
isPidRunning,
isLockValid, isLockValid,
isPidRunning,
waitForAuthentication, waitForAuthentication,
} from "../src/lib/coordination.ts"; } from "../src/lib/coordination.ts";
@ -83,12 +81,16 @@ describe("coordination", () => {
beforeEach(() => { beforeEach(() => {
// @ts-ignore - Required for testing // @ts-ignore - Required for testing
setTimeoutStub = stub(globalThis, "setTimeout", (callback: TimerHandler) => { setTimeoutStub = stub(
if (typeof callback === "function") { globalThis,
callback(); "setTimeout",
} (callback: TimerHandler) => {
return 1 as unknown as ReturnType<typeof setTimeout>; if (typeof callback === "function") {
}); callback();
}
return 1 as unknown as ReturnType<typeof setTimeout>;
},
);
}); });
afterEach(() => { afterEach(() => {
@ -100,8 +102,10 @@ describe("coordination", () => {
it("returns true when authentication completes", async () => { it("returns true when authentication completes", async () => {
// Mock fetch to simulate a successful authentication // Mock fetch to simulate a successful authentication
// @ts-ignore - Required for testing // @ts-ignore - Required for testing
fetchStub = stub(globalThis, "fetch", () => fetchStub = stub(
Promise.resolve(new Response("Auth completed", { status: 200 })) globalThis,
"fetch",
() => Promise.resolve(new Response("Auth completed", { status: 200 })),
); );
const result = await waitForAuthentication(8000); const result = await waitForAuthentication(8000);
@ -111,8 +115,10 @@ describe("coordination", () => {
it("returns false for unexpected status", async () => { it("returns false for unexpected status", async () => {
// Mock fetch to simulate an error response // Mock fetch to simulate an error response
// @ts-ignore - Required for testing // @ts-ignore - Required for testing
fetchStub = stub(globalThis, "fetch", () => fetchStub = stub(
Promise.resolve(new Response("Error", { status: 500 })) globalThis,
"fetch",
() => Promise.resolve(new Response("Error", { status: 500 })),
); );
const result = await waitForAuthentication(8000); const result = await waitForAuthentication(8000);
@ -122,8 +128,10 @@ describe("coordination", () => {
it("returns false when fetch fails", async () => { it("returns false when fetch fails", async () => {
// Mock fetch to simulate a network error // Mock fetch to simulate a network error
// @ts-ignore - Required for testing // @ts-ignore - Required for testing
fetchStub = stub(globalThis, "fetch", () => fetchStub = stub(
Promise.reject(new Error("Network error")) globalThis,
"fetch",
() => Promise.reject(new Error("Network error")),
); );
const result = await waitForAuthentication(8000); const result = await waitForAuthentication(8000);

View file

@ -1,5 +1,8 @@
import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts"; import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts";
import { DenoHttpServer, ResponseBuilder } from "../src/lib/deno-http-server.ts"; import {
DenoHttpServer,
ResponseBuilder,
} from "../src/lib/deno-http-server.ts";
import { assertEquals } from "std/assert/mod.ts"; import { assertEquals } from "std/assert/mod.ts";
describe("DenoHttpServer", () => { describe("DenoHttpServer", () => {
@ -36,7 +39,9 @@ describe("DenoHttpServer", () => {
}); });
// Send a request to the server // Send a request to the server
const response = await fetch(`http://localhost:${testPort}/test?param=value`); const response = await fetch(
`http://localhost:${testPort}/test?param=value`,
);
const text = await response.text(); const text = await response.text();
// Verify the response // Verify the response
@ -57,7 +62,9 @@ describe("DenoHttpServer", () => {
localServerInstance = server.listen(localTestPort, "localhost"); localServerInstance = server.listen(localTestPort, "localhost");
// Send a request to a non-existent route // Send a request to a non-existent route
const response = await fetch(`http://localhost:${localTestPort}/non-existent`); const response = await fetch(
`http://localhost:${localTestPort}/non-existent`,
);
// Verify the response // Verify the response
assertEquals(response.status, 404); assertEquals(response.status, 404);

View file

@ -1,6 +1,6 @@
import { assertEquals, assertRejects } from "std/assert/mod.ts"; import { assertEquals, assertRejects } from "std/assert/mod.ts";
import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts"; import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts";
import { assertSpyCalls, spy, type Spy } from "std/testing/mock.ts"; import { assertSpyCalls, type Spy, spy } from "std/testing/mock.ts";
import open from "../src/lib/deno-open.ts"; import open from "../src/lib/deno-open.ts";
// Define the expected structure returned by the mocked Deno.Command // Define the expected structure returned by the mocked Deno.Command
@ -38,7 +38,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -60,7 +62,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -76,14 +80,18 @@ describe("deno-open", () => {
it("throws error on command failure", async () => { it("throws error on command failure", async () => {
// Mock Deno.Command to return failure // Mock Deno.Command to return failure
const stderrOutput = new TextEncoder().encode("Command failed error message"); const stderrOutput = new TextEncoder().encode(
"Command failed error message",
);
const mockOutput = { const mockOutput = {
success: false, success: false,
code: 1, code: 1,
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: stderrOutput, stderr: stderrOutput,
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -105,7 +113,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -125,7 +135,7 @@ describe("deno-open", () => {
await assertRejects( await assertRejects(
() => open(url, { os: "freebsd" }), () => open(url, { os: "freebsd" }),
Error, Error,
"Unsupported platform: freebsd" "Unsupported platform: freebsd",
); );
}); });
@ -137,7 +147,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -160,7 +172,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -183,7 +197,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -205,7 +221,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -216,7 +234,13 @@ describe("deno-open", () => {
// Verify the spy was called with correct arguments // Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1); assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "cmd"); assertEquals(commandSpy.calls[0].args[0], "cmd");
assertEquals(commandSpy.calls[0].args[1]?.args, ["/c", "start", '""', "/wait", url]); assertEquals(commandSpy.calls[0].args[1]?.args, [
"/c",
"start",
'""',
"/wait",
url,
]);
}); });
it("handles background option on macOS", async () => { it("handles background option on macOS", async () => {
@ -227,7 +251,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -249,7 +275,9 @@ describe("deno-open", () => {
stdout: new Uint8Array(), stdout: new Uint8Array(),
stderr: new Uint8Array(), stderr: new Uint8Array(),
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -262,7 +290,7 @@ describe("deno-open", () => {
assertEquals(commandSpy.calls[0].args[0], "cmd"); assertEquals(commandSpy.calls[0].args[0], "cmd");
assertEquals( assertEquals(
commandSpy.calls[0].args[1]?.args, commandSpy.calls[0].args[1]?.args,
["/c", "start", '""', "https://example.com?param1=value1^&param2=value2"] ["/c", "start", '""', "https://example.com?param1=value1^&param2=value2"],
); );
}); });
@ -279,7 +307,7 @@ describe("deno-open", () => {
await assertRejects( await assertRejects(
() => open(url, { os: "darwin" }), () => open(url, { os: "darwin" }),
Error, Error,
`Failed to open "${url}": Command not found: open` `Failed to open "${url}": Command not found: open`,
); );
assertSpyCalls(commandSpy, 1); assertSpyCalls(commandSpy, 1);
}); });
@ -294,7 +322,9 @@ describe("deno-open", () => {
stdout: stdoutOutput, stdout: stdoutOutput,
stderr: stderrOutput, stderr: stderrOutput,
}; };
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) }); const mockCommandConstructor = () => ({
output: () => Promise.resolve(mockOutput),
});
commandSpy = spy(mockCommandConstructor); commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy; (Deno.Command as unknown) = commandSpy;
@ -303,7 +333,7 @@ describe("deno-open", () => {
await assertRejects( await assertRejects(
() => open(url, { os: "darwin" }), () => open(url, { os: "darwin" }),
Error, Error,
`Failed to open "${url}". Command "open ${url}" exited with code 1.\nStderr: Error details\nStdout: Additional info` `Failed to open "${url}". Command "open ${url}" exited with code 1.\nStderr: Error details\nStdout: Additional info`,
); );
assertSpyCalls(commandSpy, 1); assertSpyCalls(commandSpy, 1);
}); });

View file

@ -1,5 +1,5 @@
import { assertEquals, assertExists } from "std/assert/mod.ts"; import { assertEquals, assertExists } from "std/assert/mod.ts";
import { describe, it, beforeEach } from "std/testing/bdd.ts"; import { beforeEach, describe, it } from "std/testing/bdd.ts";
import { mcpProxy } from "../src/lib/utils.ts"; import { mcpProxy } from "../src/lib/utils.ts";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
@ -14,7 +14,7 @@ class MockTransport implements Transport {
public messages: unknown[] = []; public messages: unknown[] = [];
public errors: Error[] = []; public errors: Error[] = [];
constructor(public name: string) { } constructor(public name: string) {}
start(): Promise<void> { start(): Promise<void> {
// Mock start method - does nothing // Mock start method - does nothing

View file

@ -1,20 +1,20 @@
import { import {
assertEquals, assertEquals,
assertStringIncludes,
assertRejects, assertRejects,
assertStringIncludes,
} from "std/assert/mod.ts"; } from "std/assert/mod.ts";
import { describe, it, afterEach, beforeEach } from "std/testing/bdd.ts"; import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts";
import { import {
checkLockfile,
createLockfile,
deleteConfigFile,
deleteLockfile,
ensureConfigDir,
getConfigDir, getConfigDir,
getConfigFilePath, getConfigFilePath,
ensureConfigDir,
createLockfile,
checkLockfile,
deleteLockfile,
readJsonFile, readJsonFile,
writeJsonFile,
deleteConfigFile,
readTextFile, readTextFile,
writeJsonFile,
writeTextFile, writeTextFile,
} from "../src/lib/mcp-auth-config.ts"; } from "../src/lib/mcp-auth-config.ts";
import { MCP_REMOTE_VERSION } from "../src/lib/utils.ts"; import { MCP_REMOTE_VERSION } from "../src/lib/utils.ts";
@ -111,7 +111,7 @@ describe("mcp-auth-config", () => {
await assertRejects( await assertRejects(
() => ensureConfigDir(), () => ensureConfigDir(),
Error, Error,
"Test mkdir error" "Test mkdir error",
); );
}); });
}); });
@ -143,12 +143,14 @@ describe("mcp-auth-config", () => {
writeTextFileSpy = spy((_path: string | URL, _data: string) => { writeTextFileSpy = spy((_path: string | URL, _data: string) => {
return Promise.resolve(); return Promise.resolve();
}) as unknown as ReturnType<typeof spy<typeof Deno.writeTextFile>>; }) as unknown as ReturnType<typeof spy<typeof Deno.writeTextFile>>;
Deno.writeTextFile = writeTextFileSpy as unknown as typeof Deno.writeTextFile; Deno.writeTextFile =
writeTextFileSpy as unknown as typeof Deno.writeTextFile;
readTextFileSpy = spy((_path: string | URL) => { readTextFileSpy = spy((_path: string | URL) => {
return Promise.resolve(JSON.stringify(testData)); return Promise.resolve(JSON.stringify(testData));
}) as unknown as ReturnType<typeof spy<typeof Deno.readTextFile>>; }) as unknown as ReturnType<typeof spy<typeof Deno.readTextFile>>;
Deno.readTextFile = readTextFileSpy as unknown as typeof Deno.readTextFile; Deno.readTextFile =
readTextFileSpy as unknown as typeof Deno.readTextFile;
removeSpy = spy((_path: string | URL) => { removeSpy = spy((_path: string | URL) => {
return Promise.resolve(); return Promise.resolve();
@ -171,7 +173,10 @@ describe("mcp-auth-config", () => {
assertSpyCalls(writeTextFileSpy, 1); assertSpyCalls(writeTextFileSpy, 1);
const expectedPath = getConfigFilePath(testHash, testFilename); const expectedPath = getConfigFilePath(testHash, testFilename);
assertEquals(writeTextFileSpy.calls[0].args[0], expectedPath); assertEquals(writeTextFileSpy.calls[0].args[0], expectedPath);
assertEquals(writeTextFileSpy.calls[0].args[1], JSON.stringify(testData, null, 2)); assertEquals(
writeTextFileSpy.calls[0].args[1],
JSON.stringify(testData, null, 2),
);
// Define a schema for parsing the JSON // Define a schema for parsing the JSON
const parseFunc = { const parseFunc = {
@ -236,7 +241,11 @@ describe("mcp-auth-config", () => {
// Verify readTextFile was called // Verify readTextFile was called
assertSpyCalls(Deno.readTextFile as unknown as ReturnType<typeof spy>, 1); assertSpyCalls(Deno.readTextFile as unknown as ReturnType<typeof spy>, 1);
assertEquals((Deno.readTextFile as unknown as ReturnType<typeof spy>).calls[0].args[0], expectedPath); assertEquals(
(Deno.readTextFile as unknown as ReturnType<typeof spy>).calls[0]
.args[0],
expectedPath,
);
assertEquals(result, testText); assertEquals(result, testText);
}); });
@ -250,7 +259,7 @@ describe("mcp-auth-config", () => {
await assertRejects( await assertRejects(
() => readTextFile(testHash, testFilename, "Custom error message"), () => readTextFile(testHash, testFilename, "Custom error message"),
Error, Error,
"Custom error message" "Custom error message",
); );
}); });
}); });
@ -285,12 +294,14 @@ describe("mcp-auth-config", () => {
writeTextFileSpy = spy((_path: string | URL, _data: string) => { writeTextFileSpy = spy((_path: string | URL, _data: string) => {
return Promise.resolve(); return Promise.resolve();
}) as unknown as ReturnType<typeof spy<typeof Deno.writeTextFile>>; }) as unknown as ReturnType<typeof spy<typeof Deno.writeTextFile>>;
Deno.writeTextFile = writeTextFileSpy as unknown as typeof Deno.writeTextFile; Deno.writeTextFile =
writeTextFileSpy as unknown as typeof Deno.writeTextFile;
readTextFileSpy = spy((_path: string | URL) => { readTextFileSpy = spy((_path: string | URL) => {
return Promise.resolve(JSON.stringify(mockLockData)); return Promise.resolve(JSON.stringify(mockLockData));
}) as unknown as ReturnType<typeof spy<typeof Deno.readTextFile>>; }) as unknown as ReturnType<typeof spy<typeof Deno.readTextFile>>;
Deno.readTextFile = readTextFileSpy as unknown as typeof Deno.readTextFile; Deno.readTextFile =
readTextFileSpy as unknown as typeof Deno.readTextFile;
removeSpy = spy((_path: string | URL) => { removeSpy = spy((_path: string | URL) => {
return Promise.resolve(); return Promise.resolve();
@ -314,7 +325,9 @@ describe("mcp-auth-config", () => {
assertEquals(writeTextFileSpy.calls[0].args[0], expectedPath); assertEquals(writeTextFileSpy.calls[0].args[0], expectedPath);
// Parse the written data and verify it contains our test values // Parse the written data and verify it contains our test values
const writtenData = JSON.parse(writeTextFileSpy.calls[0].args[1] as string); const writtenData = JSON.parse(
writeTextFileSpy.calls[0].args[1] as string,
);
assertEquals(writtenData.pid, testPid); assertEquals(writtenData.pid, testPid);
assertEquals(writtenData.port, testPort); assertEquals(writtenData.port, testPort);
assertEquals(typeof writtenData.timestamp, "number"); assertEquals(typeof writtenData.timestamp, "number");

View file

@ -1,15 +1,15 @@
import { assertEquals, assertMatch, assertRejects } from "std/assert/mod.ts"; import { assertEquals, assertMatch, assertRejects } from "std/assert/mod.ts";
import { import {
AVAILABLE_PORT_START,
findAvailablePort,
getServerUrlHash, getServerUrlHash,
log, log,
MCP_REMOTE_VERSION, MCP_REMOTE_VERSION,
findAvailablePort,
setupSignalHandlers,
parseCommandLineArgs, parseCommandLineArgs,
AVAILABLE_PORT_START, setupSignalHandlers,
} from "../src/lib/utils.ts"; } from "../src/lib/utils.ts";
import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts"; import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts";
import { assertSpyCalls, spy, type MethodSpy } from "std/testing/mock.ts"; import { assertSpyCalls, type MethodSpy, spy } from "std/testing/mock.ts";
import type net from "node:net"; import type net from "node:net";
import type process from "node:process"; import type process from "node:process";
@ -99,7 +99,11 @@ describe("utils", () => {
describe("findAvailablePort", () => { describe("findAvailablePort", () => {
let mockServer: MockServer; let mockServer: MockServer;
let listenSpy: MethodSpy<MockServer, [port: number, callback: () => void], MockServer>; let listenSpy: MethodSpy<
MockServer,
[port: number, callback: () => void],
MockServer
>;
let closeSpy: MethodSpy<MockServer, [callback: () => void], MockServer>; let closeSpy: MethodSpy<MockServer, [callback: () => void], MockServer>;
beforeEach(() => { beforeEach(() => {
@ -107,21 +111,21 @@ describe("utils", () => {
mockServer = { mockServer = {
listen: (_port: number, callback: () => void) => { listen: (_port: number, callback: () => void) => {
// Properly invoke callback // Properly invoke callback
if (typeof callback === 'function') { if (typeof callback === "function") {
callback(); callback();
} }
return mockServer; return mockServer;
}, },
close: (callback: () => void) => { close: (callback: () => void) => {
// Properly invoke callback // Properly invoke callback
if (typeof callback === 'function') { if (typeof callback === "function") {
callback(); callback();
} }
return mockServer; return mockServer;
}, },
on: (_event: string, _callback: () => void) => { on: (_event: string, _callback: () => void) => {
return mockServer; return mockServer;
} },
}; };
// Create properly typed spies // Create properly typed spies
@ -137,10 +141,12 @@ describe("utils", () => {
it("returns the first available port", async () => { it("returns the first available port", async () => {
// Mock the server address method to return the expected port // Mock the server address method to return the expected port
(mockServer as unknown as { address(): { port: number } }).address = () => ({ port: AVAILABLE_PORT_START }); (mockServer as unknown as { address(): { port: number } }).address =
() => ({ port: AVAILABLE_PORT_START });
// Mock event handlers // Mock event handlers
const eventHandlers: Record<string, Array<(...args: unknown[]) => void>> = {}; const eventHandlers: Record<string, Array<(...args: unknown[]) => void>> =
{};
mockServer.on = (event: string, callback: () => void) => { mockServer.on = (event: string, callback: () => void) => {
if (!eventHandlers[event]) { if (!eventHandlers[event]) {
eventHandlers[event] = []; eventHandlers[event] = [];
@ -169,11 +175,16 @@ describe("utils", () => {
it("increments port if initial port is unavailable", async () => { it("increments port if initial port is unavailable", async () => {
// Mock the server address method to return the incremented port // Mock the server address method to return the incremented port
(mockServer as unknown as { address(): { port: number } }).address = () => ({ port: AVAILABLE_PORT_START + 1 }); (mockServer as unknown as { address(): { port: number } }).address =
() => ({ port: AVAILABLE_PORT_START + 1 });
// Mock event handlers // Mock event handlers
const eventHandlers: Record<string, Array<(...args: unknown[]) => void>> = {}; const eventHandlers: Record<string, Array<(...args: unknown[]) => void>> =
mockServer.on = (event: string, callback: (...args: unknown[]) => void) => { {};
mockServer.on = (
event: string,
callback: (...args: unknown[]) => void,
) => {
if (!eventHandlers[event]) { if (!eventHandlers[event]) {
eventHandlers[event] = []; eventHandlers[event] = [];
} }
@ -188,7 +199,9 @@ describe("utils", () => {
if (callCount === 1) { if (callCount === 1) {
// First call should fail with EADDRINUSE // First call should fail with EADDRINUSE
if (eventHandlers.error) { if (eventHandlers.error) {
const error = new Error("Address in use") as Error & { code?: string }; const error = new Error("Address in use") as Error & {
code?: string;
};
error.code = "EADDRINUSE"; error.code = "EADDRINUSE";
for (const handler of eventHandlers.error) { for (const handler of eventHandlers.error) {
handler(error); handler(error);
@ -215,8 +228,12 @@ describe("utils", () => {
it("throws after MAX_PORT_ATTEMPTS", async () => { it("throws after MAX_PORT_ATTEMPTS", async () => {
// Mock event handlers // Mock event handlers
const eventHandlers: Record<string, Array<(...args: unknown[]) => void>> = {}; const eventHandlers: Record<string, Array<(...args: unknown[]) => void>> =
mockServer.on = (event: string, callback: (...args: unknown[]) => void) => { {};
mockServer.on = (
event: string,
callback: (...args: unknown[]) => void,
) => {
if (!eventHandlers[event]) { if (!eventHandlers[event]) {
eventHandlers[event] = []; eventHandlers[event] = [];
} }
@ -227,7 +244,9 @@ describe("utils", () => {
// Always trigger error event with EADDRINUSE // Always trigger error event with EADDRINUSE
mockServer.listen = (_port: number, _callback: () => void) => { mockServer.listen = (_port: number, _callback: () => void) => {
if (eventHandlers.error) { if (eventHandlers.error) {
const error = new Error("Address in use") as Error & { code?: string }; const error = new Error("Address in use") as Error & {
code?: string;
};
error.code = "EADDRINUSE"; error.code = "EADDRINUSE";
for (const handler of eventHandlers.error) { for (const handler of eventHandlers.error) {
handler(error); handler(error);
@ -239,7 +258,7 @@ describe("utils", () => {
await assertRejects( await assertRejects(
() => findAvailablePort(mockServer as unknown as net.Server), () => findAvailablePort(mockServer as unknown as net.Server),
Error, Error,
"Timeout finding available port" "Timeout finding available port",
); );
}); });
}); });
@ -255,7 +274,9 @@ describe("utils", () => {
originalFindAvailablePort = findAvailablePort; originalFindAvailablePort = findAvailablePort;
// Mock findAvailablePort to avoid network access // Mock findAvailablePort to avoid network access
(globalThis as unknown as GlobalWithFindPort).findAvailablePort = (port: number) => Promise.resolve(port); (globalThis as unknown as GlobalWithFindPort).findAvailablePort = (
port: number,
) => Promise.resolve(port);
// Create a mock process object // Create a mock process object
globalThis.process = { globalThis.process = {
@ -269,7 +290,8 @@ describe("utils", () => {
afterEach(() => { afterEach(() => {
// Restore original process and findAvailablePort // Restore original process and findAvailablePort
globalThis.process = originalProcess; globalThis.process = originalProcess;
(globalThis as unknown as GlobalWithFindPort).findAvailablePort = originalFindAvailablePort; (globalThis as unknown as GlobalWithFindPort).findAvailablePort =
originalFindAvailablePort;
}); });
it("parses valid arguments", async () => { it("parses valid arguments", async () => {
@ -287,7 +309,8 @@ describe("utils", () => {
// Mock findAvailablePort specifically for this test // Mock findAvailablePort specifically for this test
const mockFindPort = spy(() => Promise.resolve(3000)); const mockFindPort = spy(() => Promise.resolve(3000));
// Replace the global findAvailablePort with our mock // Replace the global findAvailablePort with our mock
(globalThis as unknown as GlobalWithFindPort).findAvailablePort = mockFindPort; (globalThis as unknown as GlobalWithFindPort).findAvailablePort =
mockFindPort;
const args = ["https://example.com"]; const args = ["https://example.com"];
const defaultPort = 3000; const defaultPort = 3000;
@ -309,7 +332,7 @@ describe("utils", () => {
await parseCommandLineArgs(args, defaultPort, usage); await parseCommandLineArgs(args, defaultPort, usage);
}, },
Error, Error,
"Process exit called" "Process exit called",
); );
}); });
@ -322,7 +345,7 @@ describe("utils", () => {
async () => { async () => {
await parseCommandLineArgs(args, defaultPort, usage); await parseCommandLineArgs(args, defaultPort, usage);
}, },
Error Error,
); );
}); });
@ -335,7 +358,7 @@ describe("utils", () => {
async () => { async () => {
await parseCommandLineArgs(args, defaultPort, usage); await parseCommandLineArgs(args, defaultPort, usage);
}, },
Error Error,
); );
}); });
}); });