| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | isLoopbackHost, |
| 5 | newPreviewTabId, |
| 6 | normalizePreviewUrl, |
| 7 | PreviewUrlNormalizationError, |
| 8 | resolveAddressBarInput, |
| 9 | } from "./preview.ts"; |
| 10 | |
| 11 | describe("resolveAddressBarInput", () => { |
| 12 | it("opens what users type as addresses", () => { |
| 13 | expect(resolveAddressBarInput("localhost:5173")).toBe("http://localhost:5173/"); |
| 14 | expect(resolveAddressBarInput("127.0.0.1:3000/path")).toBe("http://127.0.0.1:3000/path"); |
| 15 | expect(resolveAddressBarInput("my-box.tailnet.ts.net")).toBe("https://my-box.tailnet.ts.net/"); |
| 16 | expect(resolveAddressBarInput("cnn.com")).toBe("https://cnn.com/"); |
| 17 | expect(resolveAddressBarInput("devbox:8080")).toBe("https://devbox:8080/"); |
| 18 | expect(resolveAddressBarInput(" https://example.com/a b ")).toBe("https://example.com/a%20b"); |
| 19 | expect(resolveAddressBarInput("example.com:8080")).toBe("https://example.com:8080/"); |
| 20 | expect(resolveAddressBarInput("[::1]:3000")).toBe("http://[::1]:3000/"); |
| 21 | expect(resolveAddressBarInput("192.168.1.5:3000")).toBe("https://192.168.1.5:3000/"); |
| 22 | }); |
| 23 | |
| 24 | it("searches for text that is not an address", () => { |
| 25 | expect(resolveAddressBarInput("weather")).toBe("https://duckduckgo.com/?q=weather"); |
| 26 | expect(resolveAddressBarInput("how to center a div")).toBe( |
| 27 | "https://duckduckgo.com/?q=how%20to%20center%20a%20div", |
| 28 | ); |
| 29 | expect(resolveAddressBarInput("what is cnn.com")).toBe( |
| 30 | "https://duckduckgo.com/?q=what%20is%20cnn.com", |
| 31 | ); |
| 32 | expect(resolveAddressBarInput("what is https://example.com")).toBe( |
| 33 | "https://duckduckgo.com/?q=what%20is%20https%3A%2F%2Fexample.com", |
| 34 | ); |
| 35 | expect(resolveAddressBarInput("what is 10:30")).toBe( |
| 36 | "https://duckduckgo.com/?q=what%20is%2010%3A30", |
| 37 | ); |
| 38 | expect(resolveAddressBarInput("note: buy milk")).toBe( |
| 39 | "https://duckduckgo.com/?q=note%3A%20buy%20milk", |
| 40 | ); |
| 41 | }); |
| 42 | |
| 43 | it("still rejects empty input and unsupported schemes", () => { |
| 44 | expect(() => resolveAddressBarInput(" ")).toThrow(PreviewUrlNormalizationError); |
| 45 | expect(() => resolveAddressBarInput("ftp://example.com")).toThrow(PreviewUrlNormalizationError); |
| 46 | expect(() => resolveAddressBarInput("mailto:alice@example.com")).toThrow( |
| 47 | PreviewUrlNormalizationError, |
| 48 | ); |
| 49 | expect(() => resolveAddressBarInput("data:text/plain,hello")).toThrow( |
| 50 | PreviewUrlNormalizationError, |
| 51 | ); |
| 52 | // A known scheme with a numeric payload is not a host and port. |
| 53 | for (const input of ["ftp:21", "tel:5551234"]) { |
| 54 | expect(() => resolveAddressBarInput(input)).toThrow(PreviewUrlNormalizationError); |
| 55 | } |
| 56 | expect(resolveAddressBarInput("devbox:8080")).toBe("https://devbox:8080/"); |
| 57 | }); |
| 58 | }); |
| 59 | |
| 60 | describe("newPreviewTabId", () => { |
| 61 | it("returns a unique tab id every call", () => { |
| 62 | const a = newPreviewTabId(); |
| 63 | const b = newPreviewTabId(); |
| 64 | expect(a).not.toBe(b); |
| 65 | expect(a.startsWith("tab_")).toBe(true); |
| 66 | }); |
| 67 | }); |
| 68 | |
| 69 | describe("isLoopbackHost", () => { |
| 70 | it.each(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"])("%s is loopback", (host) => { |
| 71 | expect(isLoopbackHost(host)).toBe(true); |
| 72 | }); |
| 73 | |
| 74 | it.each(["example.com", "192.168.1.10", "10.0.0.1", ""])("%s is not loopback", (host) => { |
| 75 | expect(isLoopbackHost(host)).toBe(false); |
| 76 | }); |
| 77 | }); |
| 78 | |
| 79 | describe("normalizePreviewUrl", () => { |
| 80 | it("treats bare loopback hosts as http", () => { |
| 81 | expect(normalizePreviewUrl("localhost:5173")).toBe("http://localhost:5173/"); |
| 82 | expect(normalizePreviewUrl("127.0.0.1:3000")).toBe("http://127.0.0.1:3000/"); |
| 83 | }); |
| 84 | |
| 85 | it("treats bare public hosts as https", () => { |
| 86 | expect(normalizePreviewUrl("example.com")).toBe("https://example.com/"); |
| 87 | }); |
| 88 | |
| 89 | it("respects explicit schemes", () => { |
| 90 | expect(normalizePreviewUrl("https://localhost:5173")).toBe("https://localhost:5173/"); |
| 91 | expect(normalizePreviewUrl("http://example.com/path?q=1")).toBe("http://example.com/path?q=1"); |
| 92 | }); |
| 93 | |
| 94 | it("rejects empty input", () => { |
| 95 | try { |
| 96 | normalizePreviewUrl(" "); |
| 97 | expect.unreachable("expected URL normalization to fail"); |
| 98 | } catch (error) { |
| 99 | expect(error).toBeInstanceOf(PreviewUrlNormalizationError); |
| 100 | expect(error).toMatchObject({ inputLength: 3, reason: "empty" }); |
| 101 | expect(error).not.toHaveProperty("rawUrl"); |
| 102 | expect("cause" in (error as object)).toBe(false); |
| 103 | } |
| 104 | }); |
| 105 | |
| 106 | it("rejects unsupported protocols", () => { |
| 107 | try { |
| 108 | normalizePreviewUrl("ftp://example.com"); |
| 109 | expect.unreachable("expected URL normalization to fail"); |
| 110 | } catch (error) { |
| 111 | expect(error).toBeInstanceOf(PreviewUrlNormalizationError); |
| 112 | expect(error).toMatchObject({ |
| 113 | inputLength: "ftp://example.com".length, |
| 114 | reason: "unsupported-protocol", |
| 115 | protocol: "ftp:", |
| 116 | }); |
| 117 | } |
| 118 | }); |
| 119 | |
| 120 | it("rejects unparseable input without retaining credentials or tokens", () => { |
| 121 | const rawUrl = "https://user:password@example.com:bad/path?access_token=secret#fragment"; |
| 122 | try { |
| 123 | normalizePreviewUrl(rawUrl); |
| 124 | expect.unreachable("expected URL normalization to fail"); |
| 125 | } catch (error) { |
| 126 | expect(error).toBeInstanceOf(PreviewUrlNormalizationError); |
| 127 | expect(error).toMatchObject({ |
| 128 | inputLength: rawUrl.length, |
| 129 | reason: "parse", |
| 130 | protocol: "https:", |
| 131 | }); |
| 132 | expect(error).not.toHaveProperty("rawUrl"); |
| 133 | expect((error as PreviewUrlNormalizationError).cause).toBeInstanceOf(Error); |
| 134 | expect((error as PreviewUrlNormalizationError).message).not.toContain( |
| 135 | ((error as PreviewUrlNormalizationError).cause as Error).message, |
| 136 | ); |
| 137 | expect((error as PreviewUrlNormalizationError).message).not.toMatch( |
| 138 | /user|password|access_token|secret|fragment/, |
| 139 | ); |
| 140 | } |
| 141 | }); |
| 142 | }); |