Improve test coverage
This commit is contained in:
parent
01a238a341
commit
7299c69a27
4 changed files with 443 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
.mcp-cli
|
||||
dist
|
||||
coverage/
|
||||
|
|
|
@ -96,4 +96,215 @@ describe("deno-open", () => {
|
|||
);
|
||||
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¶m2=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^¶m2=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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
import {
|
||||
assertEquals,
|
||||
assertStringIncludes,
|
||||
assertRejects,
|
||||
} 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 {
|
||||
getConfigDir,
|
||||
getConfigFilePath,
|
||||
ensureConfigDir,
|
||||
createLockfile,
|
||||
checkLockfile,
|
||||
deleteLockfile,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
deleteConfigFile,
|
||||
readTextFile,
|
||||
writeTextFile,
|
||||
} from "../src/lib/mcp-auth-config.ts";
|
||||
import { MCP_REMOTE_VERSION } from "../src/lib/utils.ts";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { assertSpyCalls, spy } from "std/testing/mock.ts";
|
||||
|
||||
describe("mcp-auth-config", () => {
|
||||
describe("getConfigDir", () => {
|
||||
|
@ -61,4 +72,55 @@ describe("mcp-auth-config", () => {
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,25 @@
|
|||
import { assertEquals, assertMatch } from "std/assert/mod.ts";
|
||||
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from "../src/lib/utils.ts";
|
||||
import { assertEquals, assertMatch, assertRejects } from "std/assert/mod.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 { 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("getServerUrlHash", () => {
|
||||
|
@ -74,4 +92,152 @@ describe("utils", () => {
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue