diff --git a/deno.lock b/deno.lock index c85d114..b2579b4 100644 --- a/deno.lock +++ b/deno.lock @@ -4,8 +4,10 @@ "npm:@modelcontextprotocol/sdk@*": "1.10.2_express@5.1.0_zod@3.24.3", "npm:@modelcontextprotocol/sdk@^1.9.0": "1.10.2_express@5.1.0_zod@3.24.3", "npm:@types/express@5": "5.0.1", + "npm:@types/node@*": "22.12.0", "npm:@types/node@^22.13.10": "22.15.2", "npm:@types/react@^19.0.12": "19.1.2", + "npm:express@*": "4.21.2", "npm:express@^4.21.2": "4.21.2", "npm:open@^10.1.0": "10.1.1", "npm:prettier@^3.5.3": "3.5.3", @@ -1407,21 +1409,6 @@ "workspace": { "dependencies": [ "npm:@modelcontextprotocol/sdk@*" - ], - "packageJson": { - "dependencies": [ - "npm:@modelcontextprotocol/sdk@^1.9.0", - "npm:@types/express@5", - "npm:@types/node@^22.13.10", - "npm:@types/react@^19.0.12", - "npm:express@^4.21.2", - "npm:open@^10.1.0", - "npm:prettier@^3.5.3", - "npm:react@19", - "npm:tsup@^8.4.0", - "npm:tsx@^4.19.3", - "npm:typescript@^5.8.2" - ] - } + ] } } diff --git a/tests/coordination_test.ts b/tests/coordination_test.ts new file mode 100644 index 0000000..ac7952c --- /dev/null +++ b/tests/coordination_test.ts @@ -0,0 +1,134 @@ +import { + assertEquals, + assertRejects, +} from "std/assert/mod.ts"; +import { describe, it, afterEach, beforeEach } from "std/testing/bdd.ts"; +import { assertSpyCalls, spy, stub } from "std/testing/mock.ts"; +import { + isPidRunning, + isLockValid, + waitForAuthentication, +} from "../src/lib/coordination.ts"; + +/** + * Basic tests for the coordination module + */ +describe("coordination", () => { + describe("isPidRunning", () => { + it("returns false when Deno.Command throws an error", async () => { + const originalCommand = Deno.Command; + try { + // @ts-ignore - Replace Deno.Command with a function that throws + Deno.Command = () => { + throw new Error("Test error"); + }; + + const result = await isPidRunning(1234); + assertEquals(result, false); + } finally { + // Restore original Command + Deno.Command = originalCommand; + } + }); + + if (Deno.build.os === "linux" || Deno.build.os === "darwin") { + it("checks if a process is running on non-Windows by using kill command", async () => { + // Just verify it runs without error - actual functionality depends on the OS + const result = await isPidRunning(Deno.pid); + // Our own process should be running + assertEquals(typeof result, "boolean"); + }); + } else if (Deno.build.os === "windows") { + it("checks if a process is running on Windows by using tasklist command", async () => { + // Just verify it runs without error - actual functionality depends on the OS + const result = await isPidRunning(Deno.pid); + // Our own process should be running + assertEquals(typeof result, "boolean"); + }); + } + }); + + describe("isLockValid", () => { + const mockLockData = { + pid: 1234, // A likely non-existent PID + port: 8000, + timestamp: Date.now() - (31 * 60 * 1000), // Expired (31 minutes old) + }; + + it("returns false for expired lockfile", async () => { + const result = await isLockValid(mockLockData); + assertEquals(result, false); + }); + + it("returns false for non-existent process", async () => { + // Find a PID that's unlikely to exist + let testPid = 999999; + while (await isPidRunning(testPid)) { + testPid += 1000; + } + + const validTimestampData = { + ...mockLockData, + pid: testPid, + timestamp: Date.now(), // Not expired + }; + + const result = await isLockValid(validTimestampData); + assertEquals(result, false); + }); + }); + + describe("waitForAuthentication", () => { + let fetchStub: ReturnType; + let setTimeoutStub: ReturnType; + + beforeEach(() => { + // @ts-ignore - Required for testing + setTimeoutStub = stub(globalThis, "setTimeout", (callback: TimerHandler) => { + if (typeof callback === "function") { + callback(); + } + return 1 as unknown as ReturnType; + }); + }); + + afterEach(() => { + // Clean up + if (fetchStub) fetchStub.restore(); + setTimeoutStub.restore(); + }); + + it("returns true when authentication completes", async () => { + // Mock fetch to simulate a successful authentication + // @ts-ignore - Required for testing + fetchStub = stub(globalThis, "fetch", () => + Promise.resolve(new Response("Auth completed", { status: 200 })) + ); + + const result = await waitForAuthentication(8000); + assertEquals(result, true); + }); + + it("returns false for unexpected status", async () => { + // Mock fetch to simulate an error response + // @ts-ignore - Required for testing + fetchStub = stub(globalThis, "fetch", () => + Promise.resolve(new Response("Error", { status: 500 })) + ); + + const result = await waitForAuthentication(8000); + assertEquals(result, false); + }); + + it("returns false when fetch fails", async () => { + // Mock fetch to simulate a network error + // @ts-ignore - Required for testing + fetchStub = stub(globalThis, "fetch", () => + Promise.reject(new Error("Network error")) + ); + + const result = await waitForAuthentication(8000); + assertEquals(result, false); + }); + }); +}); diff --git a/tests/test-utils.ts b/tests/test-utils.ts new file mode 100644 index 0000000..568cb6a --- /dev/null +++ b/tests/test-utils.ts @@ -0,0 +1,37 @@ +/** + * Test utilities for mcp-remote-deno tests + */ + +import type { Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +/** + * A mock server for testing + */ +export class MockServer { + /** + * The HTTP server instance + */ + server: Partial; + + /** + * A function that returns the auth code + */ + waitForAuthCode: () => Promise; + + /** + * Creates a new MockServer + * @param port The port the server is listening on + */ + constructor(port = 8000) { + this.server = { + address: () => ({ + port, + address: "127.0.0.1", + family: "IPv4" + } as AddressInfo), + // Add other server properties as needed + }; + this.waitForAuthCode = () => Promise.resolve("mock-auth-code"); + } +}