import { describe, expect, it, vi } from "vitest"; import { canonicalPeerTubeHost, channelRemoteRef, PeerTubeResolveError, resolvePeerTubeSource, videoRemoteRef, } from "./resolve.js"; const INSTANCE = "https://video.example"; const UUID = "0d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d"; function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); } describe("remote_ref canonicalization", () => { it("IDNA-lowercases hosts in channel refs", () => { expect(channelRemoteRef("alice", "VIDEO.Example.")).toBe("channel:alice@video.example"); expect(channelRemoteRef("Alice", "vĂ¯deo.example")).toBe(channelRemoteRef("alice", "xn--vdeo-5pa.example")); }); it("lowercases uuids in video refs", () => { expect(videoRemoteRef(UUID.toUpperCase(), "video.example")).toBe(`video:${UUID}@video.example`); }); it("canonicalPeerTubeHost strips brackets and trailing dots", () => { expect(canonicalPeerTubeHost("[video.example].")).toBe("video.example"); expect(canonicalPeerTubeHost("")).toBe(""); }); }); describe("resolvePeerTubeSource", () => { it("resolves a channel through the public video-channels endpoint", async () => { const fetch = vi.fn(async (url: string) => { expect(url).toBe(`${INSTANCE}/api/v1/video-channels/alice%40video.example`); return json({ id: 7, name: "alice", host: "video.example", displayName: "Alice's channel" }); }); const result = await resolvePeerTubeSource("channel", "alice", "video.example", { fetch }); expect(result).toMatchObject({ remoteRef: "channel:alice@video.example", instanceUrl: INSTANCE, live: true, label: "Alice's channel", channelId: 7, }); expect(fetch).toHaveBeenCalledOnce(); }); it("resolves a running live: isLive + PUBLISHED, deterministic master.m3u8", async () => { const fetch = vi.fn(async () => json({ uuid: UUID, name: "My live", isLive: true, state: { id: 1, label: "Published" } })); const result = await resolvePeerTubeSource("live", UUID, "video.example", { fetch }); expect(result.live).toBe(true); expect(result.playbackHlsUrl).toBe(`${INSTANCE}/static/streaming-playlists/hls/${UUID}/master.m3u8`); expect(result.remoteRef).toBe(`video:${UUID}@video.example`); }); it("a WAITING_FOR_LIVE video is not live and carries no playback URL", async () => { const fetch = vi.fn(async () => json({ uuid: UUID, name: "Waiting", isLive: true, state: { id: 4 } })); const result = await resolvePeerTubeSource("live", UUID, "video.example", { fetch }); expect(result.live).toBe(false); expect(result.playbackHlsUrl).toBeUndefined(); }); it("a VOD uses the API streamingPlaylists[].playlistUrl (type 1 only)", async () => { const fetch = vi.fn(async () => json({ uuid: UUID, name: "VOD", isLive: false, state: { id: 1 }, streamingPlaylists: [ { id: 1, type: 2, playlistUrl: "/dash/manifest.mpd" }, { id: 2, type: 1, playlistUrl: "/static/streaming-playlists/hls/x/master.m3u8" }, ], })); const result = await resolvePeerTubeSource("live", UUID, "video.example", { fetch }); expect(result.live).toBe(false); expect(result.playbackHlsUrl).toBe(`${INSTANCE}/static/streaming-playlists/hls/x/master.m3u8`); }); it("404 maps to not_found (invalid row), 500 to upstream (failed row), 403 to denied", async () => { for (const [status, reason] of [[404, "not_found"], [410, "not_found"], [500, "upstream"], [403, "denied"]] as const) { const fetch = vi.fn(async () => json({ detail: "nope" }, status)); await expect(resolvePeerTubeSource("channel", "alice", "video.example", { fetch })) .rejects.toMatchObject({ reason }); await expect(resolvePeerTubeSource("live", UUID, "video.example", { fetch })) .rejects.toMatchObject({ reason }); } }); it("transport failures are upstream, malformed payloads are upstream", async () => { const dead = vi.fn(async () => { throw new TypeError("socket hang up"); }); await expect(resolvePeerTubeSource("channel", "alice", "video.example", { fetch: dead })).rejects.toMatchObject({ reason: "upstream" }); const garbage = vi.fn(async () => new Response("not peertube", { status: 200 })); await expect(resolvePeerTubeSource("channel", "alice", "video.example", { fetch: garbage })).rejects.toBeInstanceOf(PeerTubeResolveError); const notVideo = vi.fn(async () => json({ name: "not a video" })); await expect(resolvePeerTubeSource("live", UUID, "video.example", { fetch: notVideo })).rejects.toMatchObject({ reason: "upstream" }); }); it("forwards the abort signal to the transport", async () => { const fetch = vi.fn(async (_url: string, init?: RequestInit) => { expect((init as { signal?: AbortSignal } | undefined)?.signal?.aborted).toBe(true); return json({ uuid: UUID, isLive: false, name: "x", state: { id: 1 } }); }); const controller = new AbortController(); controller.abort(); await resolvePeerTubeSource("live", UUID, "video.example", { fetch, signal: controller.signal }); expect(fetch).toHaveBeenCalledOnce(); }); it("a shortUUID is passed through to the videos endpoint", async () => { const fetch = vi.fn(async (url: string) => { expect(url.endsWith("/api/v1/videos/abc123_XY")).toBe(true); return json({ uuid: UUID, isLive: false, name: "x", state: { id: 1 } }); }); const result = await resolvePeerTubeSource("live", "abc123_XY", "video.example", { fetch }); expect(result.remoteRef).toBe(`video:${UUID}@video.example`); }); });