Files
bitvid/tests/services/s3-upload-prepare-contract.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 6e85c2a32b [storage] fix B2 / generic-S3 uploads ("S3 endpoint is required") — prepareUpload contract
Uploading to a Backblaze B2 (or any generic-S3) bucket failed with "S3 endpoint is
required." even though the connection tested fine.

Root cause: the two upload services have different prepareUpload contracts —
r2Service.prepareUpload(npub, { credentials }) vs s3UploadService.prepareUpload(settings).
MediaUploader calls both the r2 way, so for S3/B2 the npub STRING became the settings →
validateS3Connection saw no endpoint. Compounding it, a storageService.getConnection()
result keeps bucket/endpoint/region under `.meta`, which validateS3Connection reads at
the top level. (R2 worked because its service already uses the (npub, {credentials})
contract.)

Fix:
- s3UploadService.prepareUpload now accepts (npub, { credentials }) AND a direct settings
  object (back-compat), flattening meta → top-level via flattenS3Settings.
- The storage Test now derives/passes the B2 public URL (else the prefix), so it stops
  falsely warning "Public Bucket URL is missing. Uploads will fail."

Tests: tests/services/s3-upload-prepare-contract.test.mjs (3 — meta flatten, npub ignored
when credentials given, direct-settings back-compat). Build + lint clean; upload/storage
suites 46/46.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 16:02:37 -04:00

99 lines
3.8 KiB
JavaScript

// Bug: B2 (and generic-S3) uploads failed with "S3 endpoint is required." MediaUploader
// calls service.prepareUpload(npub, { credentials }) (the r2Service contract), but
// s3UploadService.prepareUpload used to take (settings, ...) — so the npub string became
// the settings (no endpoint). Also, a storageService.getConnection() result keeps
// bucket/endpoint under `.meta`, which validateS3Connection reads at the top level.
//
// Fix: s3UploadService.prepareUpload accepts (npub, { credentials }) AND flattens meta.
import "../test-helpers/setup-localstorage.mjs";
import { test, mock } from "node:test";
import assert from "node:assert/strict";
import { S3UploadService } from "../../js/services/s3UploadService.js";
function createDeps() {
const seen = {};
return {
deps: {
ensureS3SdkLoaded: mock.fn(async () => {}),
makeS3Client: mock.fn(() => ({})),
multipartUpload: mock.fn(async () => {}),
buildR2Key: mock.fn(() => "k"),
buildS3ObjectUrl: mock.fn(() => "https://u/file"),
getCorsOrigins: mock.fn(() => ["*"]),
prepareS3Connection: mock.fn(async (opts) => ({ ...opts })),
validateS3Connection: mock.fn((settings) => {
seen.settings = settings;
return { ...settings };
}),
userLogger: { warn: mock.fn(), error: mock.fn() },
buildStoragePointerValue: mock.fn(() => "p"),
buildStoragePrefixFromKey: mock.fn(() => "pre"),
getVideoNoteErrorMessage: mock.fn((c) => `Error: ${c}`),
normalizeVideoNotePayload: mock.fn((p) => ({ payload: p, errors: [] })),
calculateTorrentInfoHash: mock.fn(async () => "h"),
},
seen,
};
}
// A B2 connection as storageService.getConnection() returns it: keys + endpoint at the
// top level, but bucket/region/forcePathStyle/publicBaseUrl under `.meta`.
const B2_CONNECTION = {
provider: "backblaze_b2",
accessKeyId: "b2key",
secretAccessKey: "b2secret",
endpoint: "https://s3.us-west-004.backblazeb2.com",
forcePathStyle: false,
meta: {
provider: "backblaze_b2",
bucket: "bitvid",
region: "us-west-004",
publicBaseUrl: "https://bitvid.s3.us-west-004.backblazeb2.com",
forcePathStyle: false,
},
};
test("prepareUpload(npub, { credentials }) flattens the connection's meta to top-level settings", async () => {
const { deps, seen } = createDeps();
const service = new S3UploadService(deps);
const { settings, bucketEntry } = await service.prepareUpload("npub1abc", {
credentials: B2_CONNECTION,
});
// validateS3Connection must have received endpoint + bucket (the prior bug passed the
// npub string, so endpoint was undefined).
assert.equal(seen.settings.endpoint, "https://s3.us-west-004.backblazeb2.com");
assert.equal(seen.settings.bucket, "bitvid", "bucket flattened out of meta");
assert.equal(seen.settings.region, "us-west-004");
assert.equal(seen.settings.forcePathStyle, false);
assert.equal(
seen.settings.publicBaseUrl,
"https://bitvid.s3.us-west-004.backblazeb2.com",
"public URL flattened (so uploads don't get a blank base URL)",
);
assert.equal(bucketEntry.bucket, "bitvid");
});
test("the npub is ignored when credentials are supplied (not treated as settings)", async () => {
const { deps, seen } = createDeps();
const service = new S3UploadService(deps);
await service.prepareUpload("npub1ignored", { credentials: B2_CONNECTION });
assert.notEqual(seen.settings, "npub1ignored");
assert.equal(typeof seen.settings, "object");
});
test("still accepts a direct settings object (back-compat)", async () => {
const { deps, seen } = createDeps();
const service = new S3UploadService(deps);
await service.prepareUpload({
endpoint: "https://s3.mock",
bucket: "flat-bucket",
accessKeyId: "k",
secretAccessKey: "s",
});
assert.equal(seen.settings.bucket, "flat-bucket");
assert.equal(seen.settings.endpoint, "https://s3.mock");
});