mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
109 lines
4.4 KiB
JavaScript
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);
|
|
});
|