Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
116 lines
5.6 KiB
TypeScript
116 lines
5.6 KiB
TypeScript
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("<html>not peertube</html>", { 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`);
|
|
});
|
|
});
|