Improve test coverage

This commit is contained in:
Minoru Mizutani 2025-04-29 12:00:12 +09:00
parent 01a238a341
commit 7299c69a27
No known key found for this signature in database
4 changed files with 443 additions and 3 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules node_modules
.mcp-cli .mcp-cli
dist dist
coverage/

View file

@ -96,4 +96,215 @@ describe("deno-open", () => {
); );
assertSpyCalls(commandSpy, 1); assertSpyCalls(commandSpy, 1);
}); });
it("calls the correct command on Linux", async () => {
// Mock Deno.Command implementation to return success
const mockOutput = {
success: true,
code: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open, specifying linux in options
const url = "https://example.com";
await open(url, { os: "linux" });
// Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "xdg-open");
assertEquals(commandSpy.calls[0].args[1]?.args, [url]);
});
it("throws error for unsupported platform", async () => {
// Call open with an unsupported platform
const url = "https://example.com";
await assertRejects(
() => open(url, { os: "freebsd" }),
Error,
"Unsupported platform: freebsd"
);
});
it("uses specified app on macOS", async () => {
// Mock Deno.Command implementation to return success
const mockOutput = {
success: true,
code: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open with app option
const url = "https://example.com";
const app = "Safari";
await open(url, { os: "darwin", app: app });
// Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "open");
assertEquals(commandSpy.calls[0].args[1]?.args, ["-a", app, url]);
});
it("uses specified app on Linux", async () => {
// Mock Deno.Command implementation to return success
const mockOutput = {
success: true,
code: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open with app option
const url = "https://example.com";
const app = "firefox";
await open(url, { os: "linux", app: app });
// Verify that it uses the app directly as the command
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], app);
assertEquals(commandSpy.calls[0].args[1]?.args, [url]);
});
it("handles wait option on macOS", async () => {
// Mock Deno.Command implementation to return success
const mockOutput = {
success: true,
code: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open with wait option
const url = "https://example.com";
await open(url, { os: "darwin", wait: true });
// Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "open");
assertEquals(commandSpy.calls[0].args[1]?.args, ["-W", url]);
});
it("handles wait option on Windows", async () => {
// Mock Deno.Command implementation to return success
const mockOutput = {
success: true,
code: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open with wait option
const url = "https://example.com";
await open(url, { os: "windows", wait: true });
// Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "cmd");
assertEquals(commandSpy.calls[0].args[1]?.args, ["/c", "start", '""', "/wait", url]);
});
it("handles background option on macOS", async () => {
// Mock Deno.Command implementation to return success
const mockOutput = {
success: true,
code: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open with background option
const url = "https://example.com";
await open(url, { os: "darwin", background: true });
// Verify the spy was called with correct arguments
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "open");
assertEquals(commandSpy.calls[0].args[1]?.args, ["-g", url]);
});
it("escapes ampersands in URLs on Windows", async () => {
// Mock Deno.Command implementation to return success
const mockOutput = {
success: true,
code: 0,
stdout: new Uint8Array(),
stderr: new Uint8Array(),
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open with URL containing ampersands
const url = "https://example.com?param1=value1&param2=value2";
await open(url, { os: "windows" });
// Verify the spy was called with escaped ampersands
assertSpyCalls(commandSpy, 1);
assertEquals(commandSpy.calls[0].args[0], "cmd");
assertEquals(
commandSpy.calls[0].args[1]?.args,
["/c", "start", '""', "https://example.com?param1=value1^&param2=value2"]
);
});
it("throws error when command not found", async () => {
// Mock Deno.Command to throw NotFound error
const mockCommandConstructor = () => {
throw new Deno.errors.NotFound();
};
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open and expect it to throw
const url = "https://example.com";
await assertRejects(
() => open(url, { os: "darwin" }),
Error,
`Failed to open "${url}": Command not found: open`
);
assertSpyCalls(commandSpy, 1);
});
it("includes stdout in error message when available", async () => {
// Mock Deno.Command to return failure with stdout
const stderrOutput = new TextEncoder().encode("Error details");
const stdoutOutput = new TextEncoder().encode("Additional info");
const mockOutput = {
success: false,
code: 1,
stdout: stdoutOutput,
stderr: stderrOutput,
};
const mockCommandConstructor = () => ({ output: () => Promise.resolve(mockOutput) });
commandSpy = spy(mockCommandConstructor);
(Deno.Command as unknown) = commandSpy;
// Call open and expect it to throw with stdout included in error
const url = "https://example.com";
await assertRejects(
() => open(url, { os: "darwin" }),
Error,
`Failed to open "${url}". Command "open ${url}" exited with code 1.\nStderr: Error details\nStdout: Additional info`
);
assertSpyCalls(commandSpy, 1);
});
}); });

View file

@ -1,15 +1,26 @@
import { import {
assertEquals, assertEquals,
assertStringIncludes, assertStringIncludes,
assertRejects,
} from "std/assert/mod.ts"; } from "std/assert/mod.ts";
import { describe, it, afterEach } from "std/testing/bdd.ts"; import { describe, it, afterEach, beforeEach } from "std/testing/bdd.ts";
import { import {
getConfigDir, getConfigDir,
getConfigFilePath, getConfigFilePath,
ensureConfigDir,
createLockfile,
checkLockfile,
deleteLockfile,
readJsonFile,
writeJsonFile,
deleteConfigFile,
readTextFile,
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";
import * as path from "node:path"; import * as path from "node:path";
import * as os from "node:os"; import * as os from "node:os";
import { assertSpyCalls, spy } from "std/testing/mock.ts";
describe("mcp-auth-config", () => { describe("mcp-auth-config", () => {
describe("getConfigDir", () => { describe("getConfigDir", () => {
@ -61,4 +72,55 @@ describe("mcp-auth-config", () => {
assertEquals(filePath, expectedPath); assertEquals(filePath, expectedPath);
}); });
}); });
describe("ensureConfigDir", () => {
it("creates directory when it doesn't exist", async () => {
// Basic test without spies
await ensureConfigDir();
// If it doesn't throw, we're good
assertEquals(true, true);
});
});
describe("lockfile functions", () => {
const testHash = "testhash123";
const testPort = 12345;
const testPid = 67890;
it("can create and check lockfiles", async () => {
// Just test basic functionality without spies
await createLockfile(testHash, testPid, testPort);
const lockfile = await checkLockfile(testHash);
// Only check that data is correctly returned, not implementation details
assertEquals(lockfile?.pid, testPid);
assertEquals(lockfile?.port, testPort);
// Clean up
await deleteLockfile(testHash);
});
});
describe("file operations", () => {
const testHash = "testhash987";
const testFilename = "test-fileops.json";
const testData = { key: "value" };
it("writes and reads JSON files", async () => {
await writeJsonFile(testHash, testFilename, testData);
const parseFunc = {
parseAsync: (data: unknown) => {
return Promise.resolve(data);
},
};
const result = await readJsonFile(testHash, testFilename, parseFunc);
assertEquals((result as any)?.key, testData.key);
// Clean up
await deleteConfigFile(testHash, testFilename);
});
});
}); });

View file

@ -1,7 +1,25 @@
import { assertEquals, assertMatch } from "std/assert/mod.ts"; import { assertEquals, assertMatch, assertRejects } from "std/assert/mod.ts";
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from "../src/lib/utils.ts"; import {
getServerUrlHash,
log,
MCP_REMOTE_VERSION,
findAvailablePort,
mcpProxy,
setupSignalHandlers,
parseCommandLineArgs
} 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, spy, type MethodSpy } from "std/testing/mock.ts";
import { EventEmitter } from "node:events";
import net from "node:net";
import type { Transport } from "npm:@modelcontextprotocol/sdk/shared/transport.js";
// Define mock server type
interface MockServer {
listen: (port: number, callback: () => void) => MockServer;
close: (callback: () => void) => MockServer;
on: (event: string, callback: () => void) => MockServer;
}
describe("utils", () => { describe("utils", () => {
describe("getServerUrlHash", () => { describe("getServerUrlHash", () => {
@ -74,4 +92,152 @@ describe("utils", () => {
assertMatch(MCP_REMOTE_VERSION, /^\d+\.\d+\.\d+$/); assertMatch(MCP_REMOTE_VERSION, /^\d+\.\d+\.\d+$/);
}); });
}); });
describe("findAvailablePort", () => {
let originalNetCreateServer: typeof net.createServer;
let serverListenSpy: MethodSpy<MockServer, [port: number, callback: () => void], MockServer>;
let serverCloseSpy: MethodSpy<MockServer, [callback: () => void], MockServer>;
beforeEach(() => {
// Mock server behavior
originalNetCreateServer = net.createServer;
// Mock a server object
const mockServer: MockServer = {
listen: (port: number, callback: () => void) => {
// Call the callback to simulate server starting
callback();
return mockServer;
},
close: (callback: () => void) => {
// Call the callback to simulate server closing
callback();
return mockServer;
},
on: (_event: string, _callback: () => void) => {
return mockServer;
},
};
// Create spies on the mock server methods
serverListenSpy = spy(mockServer, "listen");
serverCloseSpy = spy(mockServer, "close");
// Mock the net.createServer
net.createServer = () => mockServer as unknown as net.Server;
});
afterEach(() => {
// Restore original net.createServer
net.createServer = originalNetCreateServer;
// Clean up spies
serverListenSpy.restore();
serverCloseSpy.restore();
});
it("finds an available port using the preferred port when it's available", async () => {
const preferredPort = 8080;
const port = await findAvailablePort(preferredPort);
assertEquals(port, preferredPort);
assertSpyCalls(serverListenSpy, 1);
assertSpyCalls(serverCloseSpy, 1);
});
it("finds an available port automatically when no preference is given", async () => {
const port = await findAvailablePort();
assertEquals(typeof port, "number");
assertSpyCalls(serverListenSpy, 1);
assertSpyCalls(serverCloseSpy, 1);
});
});
describe("parseCommandLineArgs", () => {
it("parses valid arguments", async () => {
const args = ["--server", "https://example.com", "--port", "8080"];
const defaultPort = 3000;
const usage = "Usage: mcp-remote --server <url> [--port <port>]";
const result = await parseCommandLineArgs(args, defaultPort, usage);
assertEquals(result.serverUrl, "https://example.com");
assertEquals(result.callbackPort, 8080);
});
it("uses default port if not specified", async () => {
const args = ["--server", "https://example.com"];
const defaultPort = 3000;
const usage = "Usage: mcp-remote --server <url> [--port <port>]";
const result = await parseCommandLineArgs(args, defaultPort, usage);
assertEquals(result.serverUrl, "https://example.com");
assertEquals(result.callbackPort, defaultPort);
});
it("enforces required server URL", async () => {
const args: string[] = [];
const defaultPort = 3000;
const usage = "Usage: mcp-remote --server <url> [--port <port>]";
await assertRejects(
async () => {
await parseCommandLineArgs(args, defaultPort, usage);
},
Error,
"Server URL is required"
);
});
it("handles format errors in server URL", async () => {
const args = ["--server", "not-a-url"];
const defaultPort = 3000;
const usage = "Usage: mcp-remote --server <url> [--port <port>]";
await assertRejects(
async () => {
await parseCommandLineArgs(args, defaultPort, usage);
},
Error
);
});
it("handles invalid port numbers", async () => {
const args = ["--server", "https://example.com", "--port", "invalid"];
const defaultPort = 3000;
const usage = "Usage: mcp-remote --server <url> [--port <port>]";
await assertRejects(
async () => {
await parseCommandLineArgs(args, defaultPort, usage);
},
Error
);
});
});
describe("setupSignalHandlers", () => {
it("sets up handlers for SIGINT and SIGTERM", () => {
// Create spies for process.on
const processSpy = spy(Deno, "addSignalListener");
// Mock cleanup function
const cleanup = spy(() => Promise.resolve());
// Call the function
setupSignalHandlers(cleanup);
// Verify signal handlers are set
assertSpyCalls(processSpy, 2);
assertEquals(processSpy.calls[0].args[0], "SIGINT");
assertEquals(typeof processSpy.calls[0].args[1], "function");
assertEquals(processSpy.calls[1].args[0], "SIGTERM");
assertEquals(typeof processSpy.calls[1].args[1], "function");
// Restore spy
processSpy.restore();
});
});
}); });