diff --git a/client/lib/identifier-resolver.spec.ts b/client/lib/identifier-resolver.spec.ts new file mode 100644 index 00000000..49cbf4de --- /dev/null +++ b/client/lib/identifier-resolver.spec.ts @@ -0,0 +1,403 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + isUUID, + resolveIdentifierToCreator, + resolveIdentifierToUserId, + resolveIdentifierToUsername, +} from "./identifier-resolver"; + +describe("identifier-resolver", () => { + describe("isUUID", () => { + it("should return true for valid UUIDs", () => { + expect(isUUID("550e8400-e29b-41d4-a716-446655440000")).toBe(true); + expect(isUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")).toBe(true); + expect(isUUID("f47ac10b-58cc-4372-a567-0e02b2c3d479")).toBe(true); + expect(isUUID("123e4567-e89b-12d3-a456-426614174000")).toBe(true); + }); + + it("should return true for UUIDs in different cases", () => { + expect(isUUID("550E8400-E29B-41D4-A716-446655440000")).toBe(true); + expect(isUUID("550e8400-E29B-41d4-A716-446655440000")).toBe(true); + }); + + it("should return false for invalid UUIDs", () => { + expect(isUUID("not-a-uuid")).toBe(false); + expect(isUUID("550e8400-e29b-41d4-a716")).toBe(false); // too short + expect(isUUID("550e8400-e29b-41d4-a716-446655440000-extra")).toBe(false); // too long + expect(isUUID("550e8400e29b41d4a716446655440000")).toBe(false); // missing dashes + expect(isUUID("550e8400-e29b-41d4-a716-44665544000g")).toBe(false); // invalid character 'g' + }); + + it("should return false for empty and invalid inputs", () => { + expect(isUUID("")).toBe(false); + expect(isUUID(" ")).toBe(false); + }); + + it("should return false for non-string-like UUIDs", () => { + expect(isUUID("12345678-1234-1234-1234-12345678901")).toBe(false); // one char short + expect(isUUID("12345678-1234-1234-1234-1234567890123")).toBe(false); // one char long + }); + }); + + describe("resolveIdentifierToCreator", () => { + let fetchSpy: any; + + beforeEach(() => { + // Mock the global fetch function + fetchSpy = vi.fn(); + global.fetch = fetchSpy; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should return null for empty identifier", async () => { + const result = await resolveIdentifierToCreator(""); + expect(result).toBeNull(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("should resolve username successfully", async () => { + const mockCreator = { + id: "550e8400-e29b-41d4-a716-446655440000", + username: "testuser", + email: "test@example.com", + }; + + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => mockCreator, + }); + + const result = await resolveIdentifierToCreator("testuser"); + + expect(result).toEqual(mockCreator); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith("/api/creators/testuser"); + }); + + it("should try UUID lookup when username fails and identifier is UUID", async () => { + const uuid = "550e8400-e29b-41d4-a716-446655440000"; + const mockCreator = { + id: uuid, + username: "testuser", + email: "test@example.com", + }; + + // First call (username) fails + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + // Second call (UUID) succeeds + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => mockCreator, + }); + + const result = await resolveIdentifierToCreator(uuid); + + expect(result).toEqual(mockCreator); + expect(fetchSpy).toHaveBeenCalledTimes(2); + expect(fetchSpy).toHaveBeenNthCalledWith(1, `/api/creators/${uuid}`); + expect(fetchSpy).toHaveBeenNthCalledWith(2, `/api/creators/user/${uuid}`); + }); + + it("should return null when username lookup fails and identifier is not a UUID", async () => { + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + const result = await resolveIdentifierToCreator("notauser"); + + expect(result).toBeNull(); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith("/api/creators/notauser"); + }); + + it("should return null when both username and UUID lookups fail", async () => { + const uuid = "550e8400-e29b-41d4-a716-446655440000"; + + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + const result = await resolveIdentifierToCreator(uuid); + + expect(result).toBeNull(); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("should handle fetch errors gracefully", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + fetchSpy.mockRejectedValueOnce(new Error("Network error")); + + const result = await resolveIdentifierToCreator("testuser"); + + expect(result).toBeNull(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[Identifier Resolver] Error resolving identifier:", + expect.any(Error) + ); + + consoleErrorSpy.mockRestore(); + }); + + it("should handle malformed JSON responses", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => { + throw new Error("Invalid JSON"); + }, + }); + + const result = await resolveIdentifierToCreator("testuser"); + + expect(result).toBeNull(); + expect(consoleErrorSpy).toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe("resolveIdentifierToUserId", () => { + let fetchSpy: any; + + beforeEach(() => { + fetchSpy = vi.fn(); + global.fetch = fetchSpy; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should return null for empty identifier", async () => { + const result = await resolveIdentifierToUserId(""); + expect(result).toBeNull(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("should return the UUID directly if identifier is already a UUID", async () => { + const uuid = "550e8400-e29b-41d4-a716-446655440000"; + const result = await resolveIdentifierToUserId(uuid); + + expect(result).toBe(uuid); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("should resolve username to user ID", async () => { + const mockCreator = { + id: "550e8400-e29b-41d4-a716-446655440000", + username: "testuser", + }; + + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => mockCreator, + }); + + const result = await resolveIdentifierToUserId("testuser"); + + expect(result).toBe(mockCreator.id); + expect(fetchSpy).toHaveBeenCalledWith("/api/creators/testuser"); + }); + + it("should return null when username resolution fails", async () => { + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + const result = await resolveIdentifierToUserId("nonexistent"); + + expect(result).toBeNull(); + }); + + it("should return null when API returns creator without ID", async () => { + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ username: "testuser" }), // no id field + }); + + const result = await resolveIdentifierToUserId("testuser"); + + expect(result).toBeNull(); + }); + + it("should handle errors gracefully", async () => { + fetchSpy.mockRejectedValueOnce(new Error("Network error")); + + const result = await resolveIdentifierToUserId("testuser"); + + expect(result).toBeNull(); + }); + }); + + describe("resolveIdentifierToUsername", () => { + let fetchSpy: any; + + beforeEach(() => { + fetchSpy = vi.fn(); + global.fetch = fetchSpy; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should return null for empty identifier", async () => { + const result = await resolveIdentifierToUsername(""); + expect(result).toBeNull(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("should return the username directly if identifier is not a UUID", async () => { + const result = await resolveIdentifierToUsername("testuser"); + + expect(result).toBe("testuser"); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("should resolve UUID to username", async () => { + const uuid = "550e8400-e29b-41d4-a716-446655440000"; + const mockCreator = { + id: uuid, + username: "testuser", + }; + + // First call (username endpoint with UUID) will fail + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + // Second call (UUID endpoint) will succeed + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => mockCreator, + }); + + const result = await resolveIdentifierToUsername(uuid); + + expect(result).toBe(mockCreator.username); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("should return null when UUID resolution fails", async () => { + const uuid = "550e8400-e29b-41d4-a716-446655440000"; + + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + const result = await resolveIdentifierToUsername(uuid); + + expect(result).toBeNull(); + }); + + it("should return null when API returns creator without username", async () => { + const uuid = "550e8400-e29b-41d4-a716-446655440000"; + + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + }); + + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ id: uuid }), // no username field + }); + + const result = await resolveIdentifierToUsername(uuid); + + expect(result).toBeNull(); + }); + + it("should handle errors gracefully", async () => { + const uuid = "550e8400-e29b-41d4-a716-446655440000"; + fetchSpy.mockRejectedValueOnce(new Error("Network error")); + + const result = await resolveIdentifierToUsername(uuid); + + expect(result).toBeNull(); + }); + }); + + describe("integration scenarios", () => { + let fetchSpy: any; + + beforeEach(() => { + fetchSpy = vi.fn(); + global.fetch = fetchSpy; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should handle full workflow: username -> userId -> username", async () => { + const mockCreator = { + id: "550e8400-e29b-41d4-a716-446655440000", + username: "testuser", + }; + + // First call: resolve username to creator + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => mockCreator, + }); + + const userId = await resolveIdentifierToUserId("testuser"); + expect(userId).toBe(mockCreator.id); + + // Second call: resolve userId (UUID) back to username + // This will make API calls since userId is a UUID + fetchSpy.mockResolvedValueOnce({ + ok: false, // username endpoint will fail + status: 404, + }); + + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => mockCreator, + }); + + const username = await resolveIdentifierToUsername(userId); + expect(username).toBe(mockCreator.username); + }); + + it("should handle mixed case UUIDs consistently", async () => { + const lowerUUID = "550e8400-e29b-41d4-a716-446655440000"; + const upperUUID = "550E8400-E29B-41D4-A716-446655440000"; + const mixedUUID = "550e8400-E29B-41d4-A716-446655440000"; + + expect(isUUID(lowerUUID)).toBe(true); + expect(isUUID(upperUUID)).toBe(true); + expect(isUUID(mixedUUID)).toBe(true); + + // All should be recognized as UUIDs and returned directly + expect(await resolveIdentifierToUserId(lowerUUID)).toBe(lowerUUID); + expect(await resolveIdentifierToUserId(upperUUID)).toBe(upperUUID); + expect(await resolveIdentifierToUserId(mixedUUID)).toBe(mixedUUID); + }); + }); +});