mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Adds the bucket-enumeration primitive the My Videos tab needs to find orphan and
missing files:
- s3 layer: ListObjectsV2Command wired into the SDK wrapper + a paginated
listObjects({ s3, bucket, prefix, maxKeys }) helper (follows continuation
tokens, bounded by maxKeys).
- r2Service.listVideoStorageObjects({ npub|pubkey }) — unlock-gated, lists under
the user's own u/<npub>/ prefix; returns { ok, reason, keys }. Body lives in
r2ServiceHelpers (with a shared buildProviderClient) so r2Service stays under
its file-size budget; ensureS3SdkLoaded is injectable via ctx for testing.
Tests + mutation-verified: never lists while locked (storage-locked, no call),
no-connection / missing-bucket gating, and unlocked listing is scoped to the
user's own u/<npub>/ prefix (empty-prefix mutation fails). Removing the lock gate
or widening the prefix each fail a scenario.
Build + full lint green (r2Service back under limit).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
84 lines
2.7 KiB
JavaScript
84 lines
2.7 KiB
JavaScript
// listVideoStorageObjects backs the My Videos tab's bucket reconciliation. It is
|
|
// UNLOCK-GATED — it must never attempt a bucket listing without credentials —
|
|
// and when unlocked it must list under the user's own prefix (u/<npub>/) so it
|
|
// can't enumerate (or later act on) anything outside the user's space.
|
|
|
|
import "./test-helpers/setup-localstorage.mjs";
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { listVideoStorageObjects } from "../js/services/r2ServiceHelpers.js";
|
|
|
|
const NPUB = "npub1exampleexampleexample";
|
|
|
|
function makeCtx(overrides = {}) {
|
|
const calls = { listObjects: [] };
|
|
const ctx = {
|
|
resolveConnection: async () => null,
|
|
ensureS3SdkLoaded: async () => {},
|
|
makeR2Client: () => ({}),
|
|
makeS3Client: () => ({}),
|
|
listObjects: async (args) => {
|
|
calls.listObjects.push(args);
|
|
return ["u/npub1example/a/v.mp4", "u/npub1example/b/v.mp4"];
|
|
},
|
|
...overrides,
|
|
};
|
|
return { ctx, calls };
|
|
}
|
|
|
|
const UNLOCKED = {
|
|
provider: "cloudflare_r2",
|
|
accountId: "acc",
|
|
bucket: "bkt",
|
|
accessKeyId: "AK",
|
|
secretAccessKey: "SK",
|
|
};
|
|
|
|
test("locked storage never lists (returns storage-locked)", async () => {
|
|
const { ctx, calls } = makeCtx();
|
|
const result = await listVideoStorageObjects(ctx, {
|
|
npub: NPUB,
|
|
credentials: { ...UNLOCKED, accessKeyId: "", secretAccessKey: "" },
|
|
});
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.reason, "storage-locked");
|
|
assert.equal(calls.listObjects.length, 0, "must not attempt a listing while locked");
|
|
});
|
|
|
|
test("no resolvable connection returns no-connection", async () => {
|
|
const { ctx, calls } = makeCtx();
|
|
const result = await listVideoStorageObjects(ctx, { npub: NPUB });
|
|
assert.equal(result.reason, "no-connection");
|
|
assert.equal(calls.listObjects.length, 0);
|
|
});
|
|
|
|
test("missing bucket is reported and not listed", async () => {
|
|
const { ctx, calls } = makeCtx();
|
|
const result = await listVideoStorageObjects(ctx, {
|
|
npub: NPUB,
|
|
credentials: { ...UNLOCKED, bucket: "" },
|
|
});
|
|
assert.equal(result.reason, "missing-bucket");
|
|
assert.equal(calls.listObjects.length, 0);
|
|
});
|
|
|
|
test("unlocked storage lists under the user's own u/<npub>/ prefix", async () => {
|
|
const { ctx, calls } = makeCtx();
|
|
const result = await listVideoStorageObjects(ctx, {
|
|
npub: NPUB,
|
|
credentials: UNLOCKED,
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.equal(calls.listObjects.length, 1);
|
|
assert.equal(
|
|
calls.listObjects[0].prefix,
|
|
`u/${NPUB}/`,
|
|
"must scope the listing to the user's own prefix",
|
|
);
|
|
assert.equal(calls.listObjects[0].bucket, "bkt");
|
|
assert.deepEqual(result.keys, [
|
|
"u/npub1example/a/v.mp4",
|
|
"u/npub1example/b/v.mp4",
|
|
]);
|
|
});
|