From 691aed108dfd739fce5a1aaf7b140c292653bd11 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Jan 2026 16:12:11 +0000 Subject: [PATCH] Add comprehensive tests for blossom-parser - 34 test cases covering all subcommands (servers, server, upload, list, blob, mirror, delete) - Tests for NIP-05 resolution, npub/nprofile decoding, $me alias - Tests for error handling and input validation - Tests for case insensitivity and command aliases (ls, view, rm) --- src/lib/blossom-parser.test.ts | 285 +++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/lib/blossom-parser.test.ts diff --git a/src/lib/blossom-parser.test.ts b/src/lib/blossom-parser.test.ts new file mode 100644 index 0000000..f0e77dd --- /dev/null +++ b/src/lib/blossom-parser.test.ts @@ -0,0 +1,285 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { parseBlossomCommand } from "./blossom-parser"; + +// Mock NIP-05 resolution +vi.mock("./nip05", () => ({ + isNip05: (input: string) => + input.includes("@") || /^[a-z0-9-]+\.[a-z]{2,}$/i.test(input), + resolveNip05: vi.fn(), +})); + +import { resolveNip05 } from "./nip05"; +const mockResolveNip05 = vi.mocked(resolveNip05); + +describe("parseBlossomCommand", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("servers subcommand", () => { + it("should default to servers when no args provided", async () => { + const result = await parseBlossomCommand([]); + expect(result.subcommand).toBe("servers"); + }); + + it("should parse explicit servers subcommand", async () => { + const result = await parseBlossomCommand(["servers"]); + expect(result.subcommand).toBe("servers"); + }); + }); + + describe("server subcommand", () => { + it("should parse server with URL", async () => { + const result = await parseBlossomCommand([ + "server", + "https://blossom.primal.net", + ]); + expect(result.subcommand).toBe("server"); + expect(result.serverUrl).toBe("https://blossom.primal.net"); + }); + + it("should normalize server URL without protocol", async () => { + const result = await parseBlossomCommand([ + "server", + "blossom.primal.net", + ]); + expect(result.serverUrl).toBe("https://blossom.primal.net"); + }); + + it("should preserve http:// protocol", async () => { + const result = await parseBlossomCommand([ + "server", + "http://localhost:3000", + ]); + expect(result.serverUrl).toBe("http://localhost:3000"); + }); + + it("should throw error when URL missing", async () => { + await expect(parseBlossomCommand(["server"])).rejects.toThrow( + "Server URL required", + ); + }); + }); + + describe("upload subcommand", () => { + it("should parse upload subcommand", async () => { + const result = await parseBlossomCommand(["upload"]); + expect(result.subcommand).toBe("upload"); + }); + }); + + describe("list subcommand", () => { + const testPubkey = + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; + + it("should parse list with no args (uses active account)", async () => { + const result = await parseBlossomCommand(["list"], testPubkey); + expect(result.subcommand).toBe("list"); + expect(result.pubkey).toBe(testPubkey); + }); + + it("should parse list alias 'ls'", async () => { + const result = await parseBlossomCommand(["ls"], testPubkey); + expect(result.subcommand).toBe("list"); + }); + + it("should parse list with hex pubkey", async () => { + const result = await parseBlossomCommand(["list", testPubkey]); + expect(result.pubkey).toBe(testPubkey); + }); + + it("should parse list with npub", async () => { + const npub = + "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"; + const result = await parseBlossomCommand(["list", npub]); + expect(result.pubkey).toBe(testPubkey); + }); + + it("should parse list with $me alias", async () => { + const result = await parseBlossomCommand(["list", "$me"], testPubkey); + expect(result.pubkey).toBe(testPubkey); + }); + + it("should parse list with NIP-05 identifier", async () => { + const resolvedPubkey = + "0000000000000000000000000000000000000000000000000000000000000001"; + mockResolveNip05.mockResolvedValueOnce(resolvedPubkey); + + const result = await parseBlossomCommand(["list", "fiatjaf@fiatjaf.com"]); + expect(mockResolveNip05).toHaveBeenCalledWith("fiatjaf@fiatjaf.com"); + expect(result.pubkey).toBe(resolvedPubkey); + }); + + it("should parse list with bare domain NIP-05", async () => { + const resolvedPubkey = + "0000000000000000000000000000000000000000000000000000000000000001"; + mockResolveNip05.mockResolvedValueOnce(resolvedPubkey); + + const result = await parseBlossomCommand(["list", "fiatjaf.com"]); + expect(mockResolveNip05).toHaveBeenCalledWith("fiatjaf.com"); + expect(result.pubkey).toBe(resolvedPubkey); + }); + + it("should throw error for invalid pubkey format", async () => { + mockResolveNip05.mockResolvedValueOnce(null); + + await expect(parseBlossomCommand(["list", "invalid"])).rejects.toThrow( + "Invalid pubkey format", + ); + }); + }); + + describe("blob subcommand", () => { + const validSha256 = + "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553"; + + it("should parse blob with sha256", async () => { + const result = await parseBlossomCommand(["blob", validSha256]); + expect(result.subcommand).toBe("blob"); + expect(result.sha256).toBe(validSha256); + expect(result.serverUrl).toBeUndefined(); + }); + + it("should parse blob alias 'view'", async () => { + const result = await parseBlossomCommand(["view", validSha256]); + expect(result.subcommand).toBe("blob"); + }); + + it("should parse blob with server URL", async () => { + const result = await parseBlossomCommand([ + "blob", + validSha256, + "blossom.primal.net", + ]); + expect(result.sha256).toBe(validSha256); + expect(result.serverUrl).toBe("https://blossom.primal.net"); + }); + + it("should lowercase sha256", async () => { + const upperSha256 = validSha256.toUpperCase(); + const result = await parseBlossomCommand(["blob", upperSha256]); + expect(result.sha256).toBe(validSha256); + }); + + it("should throw error when sha256 missing", async () => { + await expect(parseBlossomCommand(["blob"])).rejects.toThrow( + "SHA256 hash required", + ); + }); + + it("should throw error for invalid sha256", async () => { + await expect(parseBlossomCommand(["blob", "invalid"])).rejects.toThrow( + "Invalid SHA256 hash", + ); + }); + + it("should throw error for sha256 with wrong length", async () => { + await expect(parseBlossomCommand(["blob", "abc123"])).rejects.toThrow( + "Invalid SHA256 hash", + ); + }); + }); + + describe("mirror subcommand", () => { + it("should parse mirror with source and target", async () => { + const result = await parseBlossomCommand([ + "mirror", + "https://source.com/blob", + "target.com", + ]); + expect(result.subcommand).toBe("mirror"); + expect(result.sourceUrl).toBe("https://source.com/blob"); + expect(result.targetServer).toBe("https://target.com"); + }); + + it("should throw error when source URL missing", async () => { + await expect(parseBlossomCommand(["mirror"])).rejects.toThrow( + "Source URL and target server required", + ); + }); + + it("should throw error when target server missing", async () => { + await expect( + parseBlossomCommand(["mirror", "https://source.com/blob"]), + ).rejects.toThrow("Source URL and target server required"); + }); + }); + + describe("delete subcommand", () => { + const validSha256 = + "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553"; + + it("should parse delete with sha256 and server", async () => { + const result = await parseBlossomCommand([ + "delete", + validSha256, + "blossom.primal.net", + ]); + expect(result.subcommand).toBe("delete"); + expect(result.sha256).toBe(validSha256); + expect(result.serverUrl).toBe("https://blossom.primal.net"); + }); + + it("should parse delete alias 'rm'", async () => { + const result = await parseBlossomCommand([ + "rm", + validSha256, + "server.com", + ]); + expect(result.subcommand).toBe("delete"); + }); + + it("should throw error when sha256 missing", async () => { + await expect(parseBlossomCommand(["delete"])).rejects.toThrow( + "SHA256 hash and server required", + ); + }); + + it("should throw error when server missing", async () => { + await expect( + parseBlossomCommand(["delete", validSha256]), + ).rejects.toThrow("SHA256 hash and server required"); + }); + + it("should throw error for invalid sha256", async () => { + await expect( + parseBlossomCommand(["delete", "invalid", "server.com"]), + ).rejects.toThrow("Invalid SHA256 hash"); + }); + }); + + describe("unknown subcommand", () => { + it("should throw error with help text for unknown subcommand", async () => { + await expect(parseBlossomCommand(["unknown"])).rejects.toThrow( + /Unknown subcommand: unknown/, + ); + }); + + it("should include available subcommands in error", async () => { + try { + await parseBlossomCommand(["invalid"]); + } catch (e) { + const error = e as Error; + expect(error.message).toContain("servers"); + expect(error.message).toContain("server "); + expect(error.message).toContain("upload"); + expect(error.message).toContain("list"); + expect(error.message).toContain("blob"); + expect(error.message).toContain("mirror"); + expect(error.message).toContain("delete"); + } + }); + }); + + describe("case insensitivity", () => { + it("should handle uppercase subcommands", async () => { + const result = await parseBlossomCommand(["SERVERS"]); + expect(result.subcommand).toBe("servers"); + }); + + it("should handle mixed case subcommands", async () => { + const result = await parseBlossomCommand(["Upload"]); + expect(result.subcommand).toBe("upload"); + }); + }); +});