Files
bitvid/tests/external-url-torrent.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 cf6a830183 [upload] Derive a webseed torrent for external-URL posts (P2P for external links)
Posting an external URL used to skip the torrent/webseed benefit entirely. Now the
upload modal's external tab auto-derives a WebTorrent webseed from the URL at
publish time: fetch the remote file (CORS-permitting), stream it under a hard size
cap, compute the infoHash, and publish a magnet with the URL as the webseed (ws=).
Best-effort — CORS failure / over the cap / any error silently degrades to
URL-only. Only runs when the user didn't paste their own magnet.

- js/utils/externalUrlTorrent.js: fetchBlobWithCap (streams with a 500 MB cap,
  aborts past it, short-circuits on content-length), buildWebseedMagnet, and
  deriveExternalUrlTorrent (reuses createTorrentMetadata). Pure + injectable → unit
  tested without network/WebTorrent.
- mediaUploader.deriveTorrentForExternalUrl: derives the ws= magnet and, when
  storage is unlocked, also hosts the tiny .torrent so the webseed can actually
  bootstrap P2P (xs=) — best-effort, falls back to ws=-only.
- UploadModal.handleExternalFlow calls it; deriveExternalWebseed only hosts the
  .torrent when storage is unlocked, shows progress on the submit button, and
  never throws.

Tests: tests/external-url-torrent.test.mjs (9 — magnet assembly, name parsing,
streamed size-cap enforcement + short-circuit, fetch-failed, derive-with-injected-
hash, failure propagation). Lint + build green.

Next: the same for editing a URL in the Edit modal.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 09:08:32 -04:00

164 lines
6.6 KiB
JavaScript

// Derive a webseed magnet from an external URL (external-URL P2P). We fetch the
// remote file (best-effort, CORS-permitting), stream it under a hard size cap,
// compute the infoHash, and build a ws= magnet. Any failure must degrade to
// URL-only (the caller catches).
//
// test_integrity_note:
// change_type: ["new_tests"]
// scenarios:
// - id: SCN-external-url-torrent
// given: "a remote URL + an injected fetch/hash"
// when: "deriveExternalUrlTorrent / fetchBlobWithCap / buildWebseedMagnet run"
// then: "magnet carries the infoHash + ws=url; oversize/fetch failures throw coded errors"
// observable_outcomes:
// - "buildWebseedMagnet: xt/dn/ws present; xs only when torrentUrl given; empty on missing infoHash/url"
// - "deriveNameFromUrl extracts + decodes the filename"
// - "fetchBlobWithCap streams under the cap and aborts past it (code too-large)"
// - "content-length over cap short-circuits before downloading"
// - "non-ok response -> fetch-failed"
// - "deriveExternalUrlTorrent returns { infoHash, magnet(ws=url) } via injected hash"
// determinism_controls:
// - "injected fetchImpl (fake Response) + createMetadata stub + fileFactory; no network/WebTorrent"
// anti_cheat_rationale:
// prevents: ["hard-coded return value", "over-mocking internal logic"]
// relaxation:
// did_relax_any_assertion: false
import test from "node:test";
import { strict as assert } from "node:assert";
import {
buildWebseedMagnet,
deriveNameFromUrl,
fetchBlobWithCap,
deriveExternalUrlTorrent,
} from "../js/utils/externalUrlTorrent.js";
function streamResponse(bytes, { contentLength, contentType = "video/mp4", ok = true, status = 200, chunkSize = 4 } = {}) {
const headers = new Map();
if (contentLength !== null) {
headers.set("content-length", String(contentLength ?? bytes.length));
}
if (contentType) headers.set("content-type", contentType);
let offset = 0;
return {
ok,
status,
headers: { get: (k) => headers.get(k.toLowerCase()) ?? null },
body: {
getReader() {
return {
async read() {
if (offset >= bytes.length) return { done: true, value: undefined };
const end = Math.min(offset + chunkSize, bytes.length);
const value = bytes.slice(offset, end);
offset = end;
return { done: false, value };
},
async cancel() {},
};
},
},
};
}
test("buildWebseedMagnet: xt/dn/ws, xs only when torrentUrl is given", () => {
const m = buildWebseedMagnet({ infoHash: "abc123", url: "https://h/v.mp4", name: "v.mp4" });
assert.match(m, /^magnet:\?xt=urn:btih:abc123&dn=v\.mp4&ws=https%3A%2F%2Fh%2Fv\.mp4$/);
const withXs = buildWebseedMagnet({ infoHash: "abc", url: "https://h/v.mp4", torrentUrl: "https://h/v.torrent" });
assert.match(withXs, /&xs=https%3A%2F%2Fh%2Fv\.torrent$/);
});
test("buildWebseedMagnet: empty when infoHash or url is missing", () => {
assert.equal(buildWebseedMagnet({ url: "https://h/v.mp4" }), "");
assert.equal(buildWebseedMagnet({ infoHash: "abc" }), "");
assert.equal(buildWebseedMagnet({}), "");
});
test("deriveNameFromUrl extracts + decodes the filename", () => {
assert.equal(deriveNameFromUrl("https://h/path/My%20Clip.mp4"), "My Clip.mp4");
assert.equal(deriveNameFromUrl("https://h/path/"), "");
assert.equal(deriveNameFromUrl("not a url"), "");
});
test("fetchBlobWithCap streams the whole body under the cap", async () => {
const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const progress = [];
const blob = await fetchBlobWithCap("https://h/v.mp4", {
maxBytes: 1000,
onProgress: (p) => progress.push(p.received),
fetchImpl: async () => streamResponse(bytes, { chunkSize: 4 }),
});
assert.equal(blob.size, 10);
assert.deepEqual(progress, [4, 8, 10]);
});
test("fetchBlobWithCap aborts a stream that exceeds the cap (too-large)", async () => {
const bytes = new Uint8Array(20);
await assert.rejects(
() =>
fetchBlobWithCap("https://h/v.mp4", {
maxBytes: 8,
// no content-length header → only the streaming guard can catch it
fetchImpl: async () => streamResponse(bytes, { contentLength: null, chunkSize: 4 }),
}),
(err) => err.code === "too-large",
);
});
test("fetchBlobWithCap short-circuits on content-length over the cap (no download)", async () => {
let readerMade = false;
await assert.rejects(
() =>
fetchBlobWithCap("https://h/big.mp4", {
maxBytes: 100,
fetchImpl: async () => ({
ok: true,
status: 200,
headers: { get: (k) => (k.toLowerCase() === "content-length" ? "999999" : null) },
body: { getReader() { readerMade = true; return { read: async () => ({ done: true }) }; } },
}),
}),
(err) => err.code === "too-large" && err.total === 999999,
);
assert.equal(readerMade, false, "must not start reading the body when the size is known-too-big");
});
test("fetchBlobWithCap: non-ok response -> fetch-failed", async () => {
await assert.rejects(
() => fetchBlobWithCap("https://h/404", { fetchImpl: async () => ({ ok: false, status: 403, headers: { get: () => null } }) }),
(err) => err.code === "fetch-failed" && err.status === 403,
);
});
test("deriveExternalUrlTorrent returns infoHash + ws= magnet via the injected hasher", async () => {
const bytes = new Uint8Array([1, 2, 3, 4]);
const seen = { fileName: null, urlList: null };
const result = await deriveExternalUrlTorrent("https://host/clips/My%20Clip.mp4", {
maxBytes: 1000,
fetchImpl: async () => streamResponse(bytes),
fileFactory: (blob, name) => ({ blob, name }),
createMetadata: async (file, urlList) => {
seen.fileName = file.name;
seen.urlList = urlList;
return { infoHash: "deadbeef", torrentFile: new Uint8Array([9]) };
},
});
assert.equal(result.infoHash, "deadbeef");
assert.equal(seen.fileName, "My Clip.mp4", "file name derived from the URL");
assert.deepEqual(seen.urlList, ["https://host/clips/My%20Clip.mp4"], "url passed as the webseed");
assert.match(result.magnet, /xt=urn:btih:deadbeef/);
assert.match(result.magnet, /ws=https%3A%2F%2Fhost%2Fclips%2FMy%2520Clip\.mp4/);
assert.ok(result.torrentFile, "torrentFile returned for optional .torrent hosting");
});
test("deriveExternalUrlTorrent propagates a fetch failure (caller degrades to URL-only)", async () => {
await assert.rejects(
() =>
deriveExternalUrlTorrent("https://host/blocked.mp4", {
fetchImpl: async () => ({ ok: false, status: 0, headers: { get: () => null } }),
createMetadata: async () => ({ infoHash: "x" }),
}),
(err) => err.code === "fetch-failed",
);
});