mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 11:52:25 +00:00
test(s3): integration smoke for the upload -> publish -> cleanup round-trip
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]>
This commit is contained in:
1 parent
fdebf05e1a
commit
93b3d6da22
1 file changed
+125
@@ -0,0 +1,125 @@
|
||||
// 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`,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user