Files
bitvid/tests/services/s3-upload-optional-asset-failures.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 3d6ffe997b fix(upload): surface thumbnail/.torrent upload failures to the user
These optional-asset uploads are non-fatal, but failures were warn-only — the
user got a silently-missing thumbnail or torrent with no signal. Both upload
services now emit a 'warning' status at the point of failure and append a note
to the final 'Published' status (downgraded to warning variant) listing what
failed. Cloudflare-upload audit #5.

Test: tests/services/s3-upload-optional-asset-failures.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 20:19:27 -04:00

109 lines
4.4 KiB
JavaScript

// Scenario (SCN-upload-optional-asset-failure):
// Thumbnail and .torrent uploads are non-fatal, but their failures used to be
// swallowed (warn-only) so the user got a silently-missing asset
// (Cloudflare-upload audit #5).
// Given a video upload where the thumbnail AND torrent uploads fail,
// When uploadVideo runs and the note still publishes,
// Then the user sees a 'warning' status for each failed asset, and the final
// success status is a 'warning' that names what failed.
//
// Anti-cheat: drives the real uploadVideo control flow with the per-asset upload
// made to fail by content type; asserts the externally observable status events
// (message + variant), not internals.
import "../test-helpers/setup-localstorage.mjs";
import test from "node:test";
import assert from "node:assert/strict";
import { S3UploadService } from "../../js/services/s3UploadService.js";
function makeDeps() {
return {
ensureS3SdkLoaded: async () => {},
makeS3Client: () => ({}),
// Fail thumbnail (image/*) and torrent uploads; succeed for the video.
multipartUpload: async ({ contentType }) => {
if (
contentType &&
(contentType.startsWith("image/") || contentType === "application/x-bittorrent")
) {
throw new Error("simulated optional-asset upload failure");
}
},
buildR2Key: () => "u/npub/ns/video.mp4",
computeStorageContentHash: async () => "sha256deadbeef",
buildS3ObjectUrl: () => "https://mock.example/file",
getCorsOrigins: () => ["*"],
prepareS3Connection: async (o) => ({ ...o, bucket: "b", publicBaseUrl: "https://mock.example" }),
validateS3Connection: (s) => ({
...s,
endpoint: "https://s3.mock",
region: "auto",
accessKeyId: "k",
secretAccessKey: "s",
bucket: "b",
forcePathStyle: false,
publicBaseUrl: "https://mock.example",
}),
userLogger: { warn() {}, error() {} },
buildStoragePointerValue: () => "ptr",
buildStoragePrefixFromKey: () => "prefix",
getVideoNoteErrorMessage: (c) => `Error: ${c}`,
normalizeVideoNotePayload: (p) => ({ payload: p, errors: [] }),
calculateTorrentInfoHash: async () => "a".repeat(40),
};
}
const blob = (name, type) => ({ name, type, size: 8, slice() { return this; } });
test("failed thumbnail + torrent uploads surface as warnings and in the final status", async () => {
const service = new S3UploadService(makeDeps());
const events = [];
service.on("uploadStatus", (d) => events.push(d));
const published = await service.uploadVideo({
npub: "npub1example",
file: blob("video.mp4", "video/mp4"),
thumbnailFile: blob("thumb.jpg", "image/jpeg"),
torrentFile: blob("video.torrent", "application/x-bittorrent"),
metadata: { title: "Test" },
settings: { endpoint: "https://s3.mock", accessKeyId: "k", secretAccessKey: "s", bucket: "b" },
infoHash: "a".repeat(40),
publishVideoNote: async () => true,
});
assert.equal(published, true, "publish should still succeed despite optional-asset failures");
const warnings = events.filter((e) => e.variant === "warning").map((e) => e.message);
assert.ok(warnings.some((m) => /thumbnail/i.test(m)), `expected a thumbnail warning, got ${JSON.stringify(warnings)}`);
assert.ok(warnings.some((m) => /torrent/i.test(m)), `expected a torrent warning, got ${JSON.stringify(warnings)}`);
const final = events[events.length - 1];
assert.equal(final.variant, "warning", "final status should be a warning when assets failed");
assert.match(final.message, /Published/);
assert.match(final.message, /thumbnail/i);
assert.match(final.message, /torrent/i);
});
test("clean upload ends with a plain success status (no caveats)", async () => {
const deps = makeDeps();
deps.multipartUpload = async () => {}; // everything succeeds
const service = new S3UploadService(deps);
const events = [];
service.on("uploadStatus", (d) => events.push(d));
await service.uploadVideo({
npub: "npub1example",
file: blob("video.mp4", "video/mp4"),
thumbnailFile: blob("thumb.jpg", "image/jpeg"),
metadata: { title: "Test" },
settings: { endpoint: "https://s3.mock", accessKeyId: "k", secretAccessKey: "s", bucket: "b" },
infoHash: "a".repeat(40),
publishVideoNote: async () => true,
});
const final = events[events.length - 1];
assert.equal(final.variant, "success");
assert.doesNotMatch(final.message, /note:/i);
});