Loading packages/cli/deno.json +1 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ "@poppanator/http-constants": "npm:@poppanator/http-constants@^1.1.1", "@std/assert": "jsr:@std/assert@^1.0.13", "@std/fmt/colors": "jsr:@std/fmt@^0.224.0/colors", "@std/testing": "jsr:@std/testing@^1.0.8", "@std/dotenv": "jsr:@std/dotenv@^0.225.2", "@std/semver": "jsr:@std/semver@^1.0.5", "cli-highlight": "npm:cli-highlight@^2.1.11", Loading packages/cli/src/tunnel.test.ts 0 → 100644 +172 −0 Original line number Diff line number Diff line import type { Tunnel, TunnelOptions } from "@hongminhee/localtunnel"; import { assert, assertEquals, assertFalse, assertRejects } from "@std/assert"; import { assertSpyCall, stub } from "@std/testing/mock"; import type { Ora } from "ora"; import { command, tunnelAction } from "./tunnel.ts"; Deno.test("tunnel description", () => { // Test that the command is properly configured assert( command.getDescription().includes( "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", ), ); }); Deno.test("tunnel command validates port argument", async () => { const exitStub = stub(Deno, "exit", () => { throw new Error("Process would exit"); }); try { await assertRejects( () => command.parse(["invalid-port"]), Error, "Process would exit", ); assertSpyCall(exitStub, 0, { args: [2] }); } finally { exitStub.restore(); } }); Deno.test("tunnel successfully creates and manages tunnel", async () => { // Track function calls let openTunnelCalled = false; let openTunnelArgs: TunnelOptions[] = []; let startCalled = false; let succeedCalled = false; let succeedArgs: string[] = []; let logArgs: string[] = []; let errorArgs: string[] = []; let addSignalListenerCalled = false; let exitCalled = false; // Create a mock tunnel object const mockTunnel = { url: new URL("https://abc123.localhost.run"), localPort: 3000, pid: 12345, close: () => Promise.resolve(), }; // Create mock dependencies const mockDeps = { openTunnel: (args: TunnelOptions) => { openTunnelCalled = true; openTunnelArgs = [args]; return Promise.resolve(mockTunnel as Tunnel); }, ora: () => ({ start() { startCalled = true; return this; }, succeed(...args: string[]) { succeedCalled = true; succeedArgs = args; return this; }, fail() { return this; }, } as unknown as Ora), console: { log: (...args: string[]) => { logArgs = args; }, error: (...args: string[]) => { errorArgs = args; }, } as Console, addSignalListener: (() => { addSignalListenerCalled = true; }) as typeof Deno.addSignalListener, exit: (() => { exitCalled = true; }) as typeof Deno.exit, }; await tunnelAction({ service: undefined }, 3000, mockDeps); // Verify all the expected interactions occurred assert(openTunnelCalled); assertEquals(openTunnelArgs, [{ port: 3000, service: undefined }]); assert(startCalled); assert(succeedCalled); assertEquals(succeedArgs, [ "Your local server at 3000 is now publicly accessible:\n", ]); assertEquals(logArgs, ["https://abc123.localhost.run/"]); assertEquals(errorArgs, ["\nPress ^C to close the tunnel."]); assert(addSignalListenerCalled); assertFalse(exitCalled); }); Deno.test("tunnel fails to create a secure tunnel and handles error", async () => { const exitStub = stub(Deno, "exit", () => { throw new Error("Process would exit"); }); // Track function calls let openTunnelCalled = false; let openTunnelArgs: TunnelOptions[] = []; let startCalled = false; let failCalled = false; let failArgs: string[] = []; let addSignalListenerCalled = false; const tunnelError = new Error("Failed to create a secure tunnel."); // Create mock dependencies that simulate failure const mockDeps = { openTunnel: (args: TunnelOptions) => { openTunnelCalled = true; openTunnelArgs = [args]; return Promise.reject(tunnelError); }, ora: () => ({ start() { startCalled = true; return this; }, succeed() { return this; }, fail(...args: string[]) { failCalled = true; failArgs = args; return this; }, } as unknown as Ora), console: { log: () => {}, error: () => {}, } as Console, addSignalListener: (() => { addSignalListenerCalled = true; }) as typeof Deno.addSignalListener, exit: (() => { throw new Error("Process would exit"); }) as typeof Deno.exit, }; try { await assertRejects( () => tunnelAction({ service: undefined }, 3000, mockDeps), Error, "Process would exit", ); } finally { exitStub.restore(); } // Verify error handling interactions assert(openTunnelCalled); assertEquals(openTunnelArgs, [{ port: 3000, service: undefined }]); assert(startCalled); assert(failCalled); assertEquals(failArgs, ["Failed to create a secure tunnel."]); assertFalse(addSignalListenerCalled); }); packages/cli/src/tunnel.ts +39 −21 Original line number Diff line number Diff line Loading @@ -4,32 +4,50 @@ import ora from "ora"; const service = new EnumType(["localhost.run", "serveo.net"]); export const command = new Command() .type("service", service) .arguments("<port:integer>") .description( "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", ) .option("-s, --service <service:service>", "The localtunnel service to use.") .action(async (options, port: number) => { const spinner = ora({ export async function tunnelAction( options: { service?: "localhost.run" | "serveo.net" }, port: number, deps: { openTunnel: typeof openTunnel; ora: typeof ora; console: typeof console; addSignalListener: typeof Deno.addSignalListener; exit: typeof Deno.exit; } = { openTunnel, ora, console, addSignalListener: Deno.addSignalListener, exit: Deno.exit, }, ) { const spinner = deps.ora({ text: "Creating a secure tunnel...", discardStdin: false, }).start(); let tunnel: Tunnel; try { tunnel = await openTunnel({ port, service: options.service }); tunnel = await deps.openTunnel({ port, service: options.service }); } catch { spinner.fail("Failed to create a secure tunnel."); Deno.exit(1); deps.exit(1); } spinner.succeed( `Your local server at ${port} is now publicly accessible:\n`, ); console.log(tunnel.url.href); console.error("\nPress ^C to close the tunnel."); Deno.addSignalListener("SIGINT", async () => { deps.console.log(tunnel.url.href); deps.console.error("\nPress ^C to close the tunnel."); deps.addSignalListener("SIGINT", async () => { await tunnel.close(); }); }); } export const command = new Command() .type("service", service) .arguments("<port:integer>") .description( "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", ) .option("-s, --service <service:service>", "The localtunnel service to use.") .action(tunnelAction); Loading
packages/cli/deno.json +1 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ "@poppanator/http-constants": "npm:@poppanator/http-constants@^1.1.1", "@std/assert": "jsr:@std/assert@^1.0.13", "@std/fmt/colors": "jsr:@std/fmt@^0.224.0/colors", "@std/testing": "jsr:@std/testing@^1.0.8", "@std/dotenv": "jsr:@std/dotenv@^0.225.2", "@std/semver": "jsr:@std/semver@^1.0.5", "cli-highlight": "npm:cli-highlight@^2.1.11", Loading
packages/cli/src/tunnel.test.ts 0 → 100644 +172 −0 Original line number Diff line number Diff line import type { Tunnel, TunnelOptions } from "@hongminhee/localtunnel"; import { assert, assertEquals, assertFalse, assertRejects } from "@std/assert"; import { assertSpyCall, stub } from "@std/testing/mock"; import type { Ora } from "ora"; import { command, tunnelAction } from "./tunnel.ts"; Deno.test("tunnel description", () => { // Test that the command is properly configured assert( command.getDescription().includes( "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", ), ); }); Deno.test("tunnel command validates port argument", async () => { const exitStub = stub(Deno, "exit", () => { throw new Error("Process would exit"); }); try { await assertRejects( () => command.parse(["invalid-port"]), Error, "Process would exit", ); assertSpyCall(exitStub, 0, { args: [2] }); } finally { exitStub.restore(); } }); Deno.test("tunnel successfully creates and manages tunnel", async () => { // Track function calls let openTunnelCalled = false; let openTunnelArgs: TunnelOptions[] = []; let startCalled = false; let succeedCalled = false; let succeedArgs: string[] = []; let logArgs: string[] = []; let errorArgs: string[] = []; let addSignalListenerCalled = false; let exitCalled = false; // Create a mock tunnel object const mockTunnel = { url: new URL("https://abc123.localhost.run"), localPort: 3000, pid: 12345, close: () => Promise.resolve(), }; // Create mock dependencies const mockDeps = { openTunnel: (args: TunnelOptions) => { openTunnelCalled = true; openTunnelArgs = [args]; return Promise.resolve(mockTunnel as Tunnel); }, ora: () => ({ start() { startCalled = true; return this; }, succeed(...args: string[]) { succeedCalled = true; succeedArgs = args; return this; }, fail() { return this; }, } as unknown as Ora), console: { log: (...args: string[]) => { logArgs = args; }, error: (...args: string[]) => { errorArgs = args; }, } as Console, addSignalListener: (() => { addSignalListenerCalled = true; }) as typeof Deno.addSignalListener, exit: (() => { exitCalled = true; }) as typeof Deno.exit, }; await tunnelAction({ service: undefined }, 3000, mockDeps); // Verify all the expected interactions occurred assert(openTunnelCalled); assertEquals(openTunnelArgs, [{ port: 3000, service: undefined }]); assert(startCalled); assert(succeedCalled); assertEquals(succeedArgs, [ "Your local server at 3000 is now publicly accessible:\n", ]); assertEquals(logArgs, ["https://abc123.localhost.run/"]); assertEquals(errorArgs, ["\nPress ^C to close the tunnel."]); assert(addSignalListenerCalled); assertFalse(exitCalled); }); Deno.test("tunnel fails to create a secure tunnel and handles error", async () => { const exitStub = stub(Deno, "exit", () => { throw new Error("Process would exit"); }); // Track function calls let openTunnelCalled = false; let openTunnelArgs: TunnelOptions[] = []; let startCalled = false; let failCalled = false; let failArgs: string[] = []; let addSignalListenerCalled = false; const tunnelError = new Error("Failed to create a secure tunnel."); // Create mock dependencies that simulate failure const mockDeps = { openTunnel: (args: TunnelOptions) => { openTunnelCalled = true; openTunnelArgs = [args]; return Promise.reject(tunnelError); }, ora: () => ({ start() { startCalled = true; return this; }, succeed() { return this; }, fail(...args: string[]) { failCalled = true; failArgs = args; return this; }, } as unknown as Ora), console: { log: () => {}, error: () => {}, } as Console, addSignalListener: (() => { addSignalListenerCalled = true; }) as typeof Deno.addSignalListener, exit: (() => { throw new Error("Process would exit"); }) as typeof Deno.exit, }; try { await assertRejects( () => tunnelAction({ service: undefined }, 3000, mockDeps), Error, "Process would exit", ); } finally { exitStub.restore(); } // Verify error handling interactions assert(openTunnelCalled); assertEquals(openTunnelArgs, [{ port: 3000, service: undefined }]); assert(startCalled); assert(failCalled); assertEquals(failArgs, ["Failed to create a secure tunnel."]); assertFalse(addSignalListenerCalled); });
packages/cli/src/tunnel.ts +39 −21 Original line number Diff line number Diff line Loading @@ -4,32 +4,50 @@ import ora from "ora"; const service = new EnumType(["localhost.run", "serveo.net"]); export const command = new Command() .type("service", service) .arguments("<port:integer>") .description( "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", ) .option("-s, --service <service:service>", "The localtunnel service to use.") .action(async (options, port: number) => { const spinner = ora({ export async function tunnelAction( options: { service?: "localhost.run" | "serveo.net" }, port: number, deps: { openTunnel: typeof openTunnel; ora: typeof ora; console: typeof console; addSignalListener: typeof Deno.addSignalListener; exit: typeof Deno.exit; } = { openTunnel, ora, console, addSignalListener: Deno.addSignalListener, exit: Deno.exit, }, ) { const spinner = deps.ora({ text: "Creating a secure tunnel...", discardStdin: false, }).start(); let tunnel: Tunnel; try { tunnel = await openTunnel({ port, service: options.service }); tunnel = await deps.openTunnel({ port, service: options.service }); } catch { spinner.fail("Failed to create a secure tunnel."); Deno.exit(1); deps.exit(1); } spinner.succeed( `Your local server at ${port} is now publicly accessible:\n`, ); console.log(tunnel.url.href); console.error("\nPress ^C to close the tunnel."); Deno.addSignalListener("SIGINT", async () => { deps.console.log(tunnel.url.href); deps.console.error("\nPress ^C to close the tunnel."); deps.addSignalListener("SIGINT", async () => { await tunnel.close(); }); }); } export const command = new Command() .type("service", service) .arguments("<port:integer>") .description( "Expose a local HTTP server to the public internet using a secure tunnel.\n\n" + "Note that the HTTP requests through the tunnel have X-Forwarded-* headers.", ) .option("-s, --service <service:service>", "The localtunnel service to use.") .action(tunnelAction);