import { describe, it, expect } from "vitest"; import LinkUtils from "@/js/LinkUtils"; describe("LinkUtils.js", () => { describe("renderReticulumLinks", () => { it("detects nomadnet:// links with hash and path", () => { const text = "nomadnet://1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"; const result = LinkUtils.renderReticulumLinks(text); expect(result).toContain('class="nomadnet-link'); expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"'); }); it("detects nomadnet@ links", () => { const text = "nomadnet@1dfeb0d794963579bd21ac8f153c77a4"; const result = LinkUtils.renderReticulumLinks(text); expect(result).toContain('class="nomadnet-link'); expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"'); }); it("detects bare hash and path links as nomadnet", () => { const text = "1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"; const result = LinkUtils.renderReticulumLinks(text); expect(result).toContain('class="nomadnet-link'); expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"'); }); it("detects bare hash as lxmf", () => { const text = "1dfeb0d794963579bd21ac8f153c77a4"; const result = LinkUtils.renderReticulumLinks(text); expect(result).toContain('class="lxmf-link'); expect(result).toContain('data-lxmf-address="1dfeb0d794963579bd21ac8f153c77a4"'); }); it("detects lxmf:// links", () => { const text = "lxmf://1dfeb0d794963579bd21ac8f153c77a4"; const result = LinkUtils.renderReticulumLinks(text); expect(result).toContain('class="lxmf-link'); expect(result).toContain('data-lxmf-address="1dfeb0d794963579bd21ac8f153c77a4"'); }); it("detects lxmf@ links", () => { const text = "lxmf@1dfeb0d794963579bd21ac8f153c77a4"; const result = LinkUtils.renderReticulumLinks(text); expect(result).toContain('class="lxmf-link'); expect(result).toContain('data-lxmf-address="1dfeb0d794963579bd21ac8f153c77a4"'); }); }); describe("renderStandardLinks", () => { it("detects http links", () => { const text = "visit http://example.com"; const result = LinkUtils.renderStandardLinks(text); expect(result).toMatch(/ { const text = "visit https://example.com/path?query=1"; const result = LinkUtils.renderStandardLinks(text); expect(result).toContain(' { const result = LinkUtils.renderStandardLinks("visit https://example.com/path?x=1, now"); expect(result).toContain('href="https://example.com/path?x=1"'); expect(result).toContain(", now"); }); it("keeps balanced parenthesis in url but trims unmatched trailing one", () => { const withBalanced = LinkUtils.renderStandardLinks("see https://example.com/path_(v1)"); expect(withBalanced).toContain('href="https://example.com/path_(v1)"'); const withTrailing = LinkUtils.renderStandardLinks("see (https://example.com/path_(v1))"); expect(withTrailing).toContain('href="https://example.com/path_(v1)"'); expect(withTrailing).toContain(")"); }); it("keeps escaped entity query content in href", () => { const text = "visit https://example.com/search?q=a&lang=en"; const result = LinkUtils.renderStandardLinks(text); expect(result).toContain('href="https://example.com/search?q=a&lang=en"'); }); }); describe("renderAllLinks", () => { it("detects both types of links", () => { const text = "Check https://google.com and nomadnet://1dfeb0d794963579bd21ac8f153c77a4"; const result = LinkUtils.renderAllLinks(text); expect(result).toMatch(/ { expect(LinkUtils.renderAllLinks("")).toBe(""); }); it("returns string with no links for plain text", () => { const result = LinkUtils.renderAllLinks("Just some words."); expect(result).toContain("Just some words."); expect(result).not.toContain(" { const original = 'already linked https://example.com'; const result = LinkUtils.renderAllLinks(original); expect(result).toBe(original); expect((result.match(/ { const text = "1dfeb0d794963579bd21ac8f153c77a4:/page/meshchatx_on_pi.mu"; const result = LinkUtils.renderAllLinks(text); expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/meshchatx_on_pi.mu"'); }); }); describe("risky: no script or data URLs in href", () => { it("does not produce javascript: href for text containing javascript: URL", () => { const text = "see javascript:alert(1) here"; const result = LinkUtils.renderStandardLinks(text); expect(result).not.toMatch(/\bhref\s*=\s*["']?\s*javascript:/i); }); it("does not produce data: href for text containing data: URL", () => { const text = "see data:text/html, here"; const result = LinkUtils.renderStandardLinks(text); expect(result).not.toMatch(/\bhref\s*=\s*["']?\s*data\s*:/i); }); it("does not produce data: href for data image payload text", () => { const text = "inline image data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA"; const result = LinkUtils.renderStandardLinks(text); expect(result).not.toMatch(/\bhref\s*=\s*["']?\s*data\s*:/i); expect(result).not.toContain(" { const text = "https://example.com javascript:alert(1)"; const result = LinkUtils.renderStandardLinks(text); expect(result).toMatch(/ { const text = 'see https://evil.example/path" onmouseover="alert(1) ok'; const result = LinkUtils.renderStandardLinks(text); const open = result.indexOf("", open); const openTag = result.slice(open, openEnd + 1); expect(openTag).not.toMatch(/onmouseover/i); expect(openTag).not.toMatch(/javascript:/i); }); it("handles null and undefined without throwing", () => { expect(LinkUtils.renderReticulumLinks(null)).toBe(""); expect(LinkUtils.renderReticulumLinks(undefined)).toBe(""); expect(LinkUtils.renderStandardLinks(null)).toBe(""); }); it("handles very long input without hanging", () => { const long = "a".repeat(100000); const start = Date.now(); LinkUtils.renderAllLinks(long); expect(Date.now() - start).toBeLessThan(200); }); }); describe("httpUrlHrefOrNull", () => { it("returns canonical https href", () => { expect(LinkUtils.httpUrlHrefOrNull("https://example.com/path")).toBe("https://example.com/path"); }); it("returns null for javascript: payloads that start with https-looking junk", () => { expect(LinkUtils.httpUrlHrefOrNull("https://example.com javascript:alert(1)")).toBeNull(); }); it("returns null for non-http schemes", () => { expect(LinkUtils.httpUrlHrefOrNull("javascript:alert(1)")).toBeNull(); expect(LinkUtils.httpUrlHrefOrNull("file:///etc/passwd")).toBeNull(); }); }); describe("fuzzing robustness", () => { it("httpUrlHrefOrNull and renderAllLinks tolerate random BMP strings", () => { for (let i = 0; i < 300; i++) { let s = ""; const len = Math.floor(Math.random() * 600); for (let j = 0; j < len; j++) { s += String.fromCharCode(Math.floor(Math.random() * 65536)); } expect(() => LinkUtils.httpUrlHrefOrNull(s)).not.toThrow(); expect(() => LinkUtils.renderAllLinks(s)).not.toThrow(); const h = LinkUtils.httpUrlHrefOrNull(s); if (h !== null) { expect(h.startsWith("http://") || h.startsWith("https://")).toBe(true); } const rendered = LinkUtils.renderAllLinks(s); expect(rendered.toLowerCase()).not.toContain("