mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Drives the real s3UploadService.uploadVideo for a generic (vhost-style) S3 connection with only the S3 I/O mocked, then feeds the published note's URLs into r2Service.deleteVideoStorage. Asserts the upload writes video + .thumb + .torrent, the note is valid (URL + magnet with info-hash), and cleanup deletes exactly those objects using a forcePathStyle-aware S3 client — i.e. the upload key and the cleanup-derived key agree (the parity property). Co-Authored-By: Claude Opus 4.8 <[email protected]>
126 lines
5.0 KiB
JavaScript
126 lines
5.0 KiB
JavaScript
// Scenario (SCN-s3-publish-cleanup-roundtrip):
|
|
// End-to-end parity smoke for GENERIC S3 (virtual-hosted style). Drives the
|
|
// real s3UploadService.uploadVideo with only the S3 I/O boundary mocked
|
|
// (makeS3Client + multipartUpload), using the REAL key/URL/magnet/note
|
|
// builders, then feeds the resulting published note's URLs into
|
|
// r2Service.deleteVideoStorage. Asserts:
|
|
// - the upload writes the expected objects (video + .thumb + .torrent),
|
|
// - the published note is valid (has the URL + a magnet with the info-hash),
|
|
// - cleanup deletes EXACTLY the objects the upload wrote (the upload key and
|
|
// the cleanup-derived key agree) — using a forcePathStyle-aware S3 client.
|
|
// This is the round-trip that makes generic-S3 delete/edit cleanup actually
|
|
// work, the parity gap that previously broke for vhost-style buckets.
|
|
|
|
import "../test-helpers/setup-localstorage.mjs";
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__ = true;
|
|
const { S3UploadService } = await import("../../js/services/s3UploadService.js");
|
|
const { R2Service } = await import("../../js/services/r2Service.js");
|
|
|
|
const PUBLIC_BASE = "https://cdn.example";
|
|
const ENDPOINT = "https://s3.us-east-1.amazonaws.com";
|
|
const NPUB = "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsx5h2j";
|
|
const INFO_HASH = "a".repeat(40);
|
|
|
|
const S3_SETTINGS = {
|
|
provider: "s3",
|
|
endpoint: ENDPOINT,
|
|
region: "us-east-1",
|
|
accessKeyId: "AK",
|
|
secretAccessKey: "SK",
|
|
bucket: "my-bucket",
|
|
forcePathStyle: false, // virtual-hosted-style — the case that used to break
|
|
publicBaseUrl: PUBLIC_BASE,
|
|
};
|
|
|
|
const fileLike = (name, type) => ({ name, type, size: 1024, slice() { return this; } });
|
|
|
|
test("a generic-S3 upload publishes a valid note whose objects deleteVideoStorage can clean up", async () => {
|
|
// --- 1. Upload via the real s3UploadService (only S3 I/O mocked) ---
|
|
const uploadedKeys = [];
|
|
let publishedPayload = null;
|
|
|
|
const svc = new S3UploadService({
|
|
ensureS3SdkLoaded: async () => {},
|
|
makeS3Client: () => ({ __mock: "s3" }),
|
|
multipartUpload: async ({ key }) => {
|
|
uploadedKeys.push(key);
|
|
},
|
|
calculateTorrentInfoHash: async () => INFO_HASH,
|
|
userLogger: { warn() {}, error() {} },
|
|
// buildR2Key, buildS3ObjectUrl, normalizeVideoNotePayload,
|
|
// buildStoragePointer*, getVideoNoteErrorMessage stay REAL (defaultDeps).
|
|
});
|
|
|
|
const ok = await svc.uploadVideo({
|
|
npub: NPUB,
|
|
file: fileLike("clip.mp4", "video/mp4"),
|
|
thumbnailFile: fileLike("clip.jpg", "image/jpeg"),
|
|
torrentFile: fileLike("clip.torrent", "application/x-bittorrent"),
|
|
metadata: { title: "Parity Smoke" },
|
|
infoHash: INFO_HASH,
|
|
settings: S3_SETTINGS,
|
|
publishVideoNote: async (payload) => {
|
|
publishedPayload = payload;
|
|
return true;
|
|
},
|
|
});
|
|
|
|
assert.equal(ok, true, "upload+publish should succeed");
|
|
assert.equal(uploadedKeys.length, 3, "uploads video + thumbnail + torrent");
|
|
|
|
const note = publishedPayload?.legacyFormData;
|
|
assert.ok(note, "published a normalized video note");
|
|
assert.ok(
|
|
note.url.startsWith(`${PUBLIC_BASE}/`),
|
|
`note URL should be under the bucket base: ${note.url}`,
|
|
);
|
|
assert.match(note.magnet, new RegExp(`btih:${INFO_HASH}`), "magnet carries the info-hash");
|
|
assert.match(note.magnet, /ws=/, "magnet has a web-seed (the hosted URL)");
|
|
assert.ok(note.thumbnail.startsWith(`${PUBLIC_BASE}/`), "thumbnail URL under the base");
|
|
|
|
// --- 2. Clean up via the real r2Service.deleteVideoStorage, S3 connection ---
|
|
const deletedKeys = [];
|
|
const clients = [];
|
|
const r2 = new R2Service({
|
|
makeR2Client: (args) => { clients.push({ kind: "r2", args }); return {}; },
|
|
makeS3Client: (args) => { clients.push({ kind: "s3", args }); return {}; },
|
|
deleteObject: async ({ key }) => { deletedKeys.push(key); },
|
|
});
|
|
r2.resolveConnection = async () => S3_SETTINGS;
|
|
|
|
const cleanup = await r2.deleteVideoStorage({
|
|
npub: NPUB,
|
|
videos: [{ url: note.url, thumbnail: note.thumbnail }],
|
|
});
|
|
|
|
assert.equal(cleanup.skipped, false);
|
|
// Generic S3 => S3 client honoring forcePathStyle: false (not the R2 path-style).
|
|
assert.equal(clients[0]?.kind, "s3");
|
|
assert.equal(clients[0]?.args.forcePathStyle, false);
|
|
|
|
// The round-trip property: cleanup deletes exactly the objects we uploaded
|
|
// (video + its .torrent + thumbnail). The video + thumbnail keys must match
|
|
// what the upload wrote; the .torrent is derived from the video key.
|
|
const uploadedSet = new Set(uploadedKeys);
|
|
const torrentKey = uploadedKeys
|
|
.find((k) => /\.mp4$/.test(k))
|
|
.replace(/\.[^/.]+$/, ".torrent");
|
|
for (const k of deletedKeys) {
|
|
assert.ok(
|
|
uploadedSet.has(k) || k === torrentKey,
|
|
`cleanup deleted ${k}, which was not uploaded`,
|
|
);
|
|
}
|
|
// Specifically: the video object and the thumbnail object are both removed.
|
|
for (const uploaded of uploadedKeys) {
|
|
if (uploaded === torrentKey) continue; // torrent re-derived; covered above
|
|
assert.ok(
|
|
deletedKeys.includes(uploaded),
|
|
`uploaded object ${uploaded} should be cleaned up`,
|
|
);
|
|
}
|
|
});
|