Files
bitvid/tests/storage-cors-help.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 d7ad151698 [ui] #38: add a CORS setup helper modal for B2 / S3-compatible buckets
Confirmed against the real B2 account: the web-console "Share everything..." presets
write DOWNLOAD-only CORS (s3_get/s3_head, b2_download_*) with NO s3_put/s3_post, so
bitvid's browser uploads CORS-fail. The web UI can't set custom rules — it points
users at the B2 CLI.

Adds a "CORS setup help" button (shown for B2 / Custom S3, hidden for R2) that opens a
modal with:
- the exact CORS rules JSON in B2-native shape (upload + ranged-playback ops; origins
  pre-filled from getCorsOrigins()), with a Copy button,
- the `b2 update-bucket --corsRules '<json>' <bucket> allPublic` command to apply it,
- step-by-step + the "use a key with writeBucketCors and bitvid sets it for you" tip.

CORS logic extracted to js/ui/profileModal/storageCorsHelp.js (pure builders +
StorageCorsHelp modal controller) so ProfileStorageController stays under the 1000-line
cap. Tests: tests/storage-cors-help.test.mjs (8 — asserts the rules include the upload
ops the presets omit, origin pre-fill, command shape, modal open/close/visibility).
Build + lint clean; profile-modal + storage suites 32/32.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 10:37:24 -04:00

127 lines
4.7 KiB
JavaScript

// Storage CORS setup helper (B2 / S3-compatible): the rules + command bitvid shows a
// user so in-browser uploads work. The crucial property: the rules include the UPLOAD
// operations (s3_put / s3_post) that B2's web-console "share" presets omit — that gap
// is exactly why uploads CORS-fail until these custom rules are applied.
import test, { beforeEach, afterEach } from "node:test";
import { strict as assert } from "node:assert";
import { JSDOM } from "jsdom";
import {
buildBucketCorsRules,
buildB2CorsCommand,
StorageCorsHelp,
} from "../js/ui/profileModal/storageCorsHelp.js";
test("CORS rules include the upload operations the B2 presets omit", () => {
const [rule] = buildBucketCorsRules(["https://bitvid.network"]);
for (const op of ["s3_put", "s3_post"]) {
assert.ok(
rule.allowedOperations.includes(op),
`rule must allow ${op} (browser upload) — the whole point of this helper`,
);
}
// ...and still allow ranged playback reads.
assert.ok(rule.allowedOperations.includes("s3_get"));
assert.ok(rule.allowedOperations.includes("s3_head"));
assert.deepEqual(rule.exposeHeaders, [
"ETag",
"Content-Length",
"Content-Range",
"Accept-Ranges",
]);
});
test("CORS rules use the given origins, and fall back to every-origin when none", () => {
assert.deepEqual(
buildBucketCorsRules(["https://a.example", "https://b.example"])[0]
.allowedOrigins,
["https://a.example", "https://b.example"],
);
assert.deepEqual(buildBucketCorsRules([])[0].allowedOrigins, ["*"]);
assert.deepEqual(buildBucketCorsRules(undefined)[0].allowedOrigins, ["*"]);
});
test("the B2 CLI command targets the bucket as allPublic with compact one-line JSON", () => {
const rules = buildBucketCorsRules(["https://bitvid.network"]);
const cmd = buildB2CorsCommand("bitvid", rules);
assert.match(cmd, /^b2 update-bucket --corsRules '.*' bitvid allPublic$/);
assert.ok(!cmd.includes("\n"), "command must be a single pasteable line");
// The embedded JSON is valid and carries the upload op.
const json = cmd.slice(cmd.indexOf("'") + 1, cmd.lastIndexOf("'"));
assert.ok(JSON.parse(json)[0].allowedOperations.includes("s3_put"));
});
test("a missing bucket name degrades to an obvious placeholder (not an empty arg)", () => {
assert.match(buildB2CorsCommand("", buildBucketCorsRules([])), /YOUR_BUCKET allPublic$/);
});
// --- DOM behavior ---
let dom;
beforeEach(() => {
dom = new JSDOM(
`<!doctype html><html><body>
<button id="storageCorsHelpBtn" class="hidden"></button>
<div id="storageCorsModal" class="hidden">
<div class="modal-sheet" tabindex="-1"></div>
<button data-storage-cors-dismiss id="x"></button>
<code id="storageCorsJson"></code>
<code id="storageCorsCli"></code>
<button id="storageCorsCopyJsonBtn"></button>
<button id="storageCorsCopyCmdBtn"></button>
</div>
</body></html>`,
{ url: "https://unstable.bitvid.network/" },
);
globalThis.document = dom.window.document;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.window = dom.window;
});
afterEach(() => {
delete globalThis.document;
delete globalThis.HTMLElement;
delete globalThis.window;
});
test("open() fills the JSON + command from the current origin and reveals the modal", () => {
const help = new StorageCorsHelp({ getBucket: () => "bitvid" });
help.cacheDom(dom.window.document);
help.registerEventListeners();
help.open();
const modal = dom.window.document.getElementById("storageCorsModal");
assert.ok(!modal.classList.contains("hidden"), "modal is shown");
const json = dom.window.document.getElementById("storageCorsJson").textContent;
const rule = JSON.parse(json)[0];
assert.deepEqual(
rule.allowedOrigins,
["https://unstable.bitvid.network"],
"origins pre-filled from getCorsOrigins()",
);
const cli = dom.window.document.getElementById("storageCorsCli").textContent;
assert.match(cli, /bitvid allPublic$/);
});
test("dismiss controls hide the modal again", () => {
const help = new StorageCorsHelp({ getBucket: () => "bitvid" });
help.cacheDom(dom.window.document);
help.registerEventListeners();
help.open();
dom.window.document.getElementById("x").click();
assert.ok(
dom.window.document.getElementById("storageCorsModal").classList.contains("hidden"),
);
});
test("setVisible toggles the help button for S3-compatible vs R2", () => {
const help = new StorageCorsHelp({ getBucket: () => "" });
help.cacheDom(dom.window.document);
const btn = dom.window.document.getElementById("storageCorsHelpBtn");
help.setVisible(true);
assert.ok(!btn.classList.contains("hidden"));
help.setVisible(false);
assert.ok(btn.classList.contains("hidden"));
});