Files
bitvid/tests/storage-connections.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 f0beab2713 [ui] store storage credentials per provider so R2 / B2 / S3 don't clash
The Storage pane saved every provider into a single shared "default" connection id,
so configuring Backblaze B2 overwrote your Cloudflare R2 (and vice-versa). The service
layer, upload path, and sync already supported a MAP of connections keyed by id
(listConnections / defaultForUploads / exportAccountRecord) — only the UI collapsed
them into one slot.

- Save each provider under its own slot keyed by provider id (cloudflare_r2 /
  backblaze_b2 / generic_s3), preserving a single active defaultForUploads and lazily
  migrating the legacy "default" slot to its provider id.
- Switching the provider dropdown now loads that provider's saved credentials (or
  clears the fields for a fresh entry), so each provider keeps its own creds visibly.
- Sync is unchanged: it already exports the whole account record, so every provider's
  connection syncs together.

Logic extracted to js/ui/profileModal/storageConnections.js (saveProviderConnection +
pure helpers) so ProfileStorageController stays under the 1000-line cap. Tests:
tests/storage-connections.test.mjs (7 — pure default/dup logic + integration proving
R2+B2 coexist with one default, the re-default switch, legacy-"default" migration, and
that the synced record carries both). Storage + profile-modal suites 69/69; lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 10:58:30 -04:00

186 lines
8.0 KiB
JavaScript

// Per-provider storage connections: each provider type (R2 / B2 / Custom S3) must keep
// its OWN credentials so saving one never overwrites another, and the saved set syncs
// as a whole. The UI used to collapse everything into one shared "default" slot.
import { test, describe, beforeEach } from "node:test";
import assert from "node:assert";
import * as fakeIndexedDB from "fake-indexeddb";
if (!globalThis.indexedDB) {
globalThis.indexedDB = fakeIndexedDB.indexedDB;
globalThis.IDBKeyRange = fakeIndexedDB.IDBKeyRange;
}
import storageService from "../js/services/storageService.js";
import {
connectionProvider,
findProviderConnection,
computeDefaultForUploads,
legacyDuplicateIds,
saveProviderConnection,
} from "../js/ui/profileModal/storageConnections.js";
// --- Pure decision logic ---
describe("per-provider connection helpers", () => {
test("computeDefaultForUploads: explicit choice, first-ever, and 'was already default'", () => {
// Explicit checkbox always wins.
assert.equal(
computeDefaultForUploads({ isDefault: true, connections: [{ id: "x" }], provider: "backblaze_b2" }),
true,
);
// The very first connection becomes the default automatically.
assert.equal(
computeDefaultForUploads({ isDefault: false, connections: [], provider: "backblaze_b2" }),
true,
);
// Re-saving the provider that was already the default keeps it default...
assert.equal(
computeDefaultForUploads({
isDefault: false,
connections: [{ id: "cloudflare_r2", provider: "cloudflare_r2", meta: { defaultForUploads: true } }],
provider: "cloudflare_r2",
}),
true,
);
// ...but a non-default provider stays non-default (won't steal it).
assert.equal(
computeDefaultForUploads({
isDefault: false,
connections: [
{ id: "cloudflare_r2", provider: "cloudflare_r2", meta: { defaultForUploads: true } },
{ id: "backblaze_b2", provider: "backblaze_b2", meta: { defaultForUploads: false } },
],
provider: "backblaze_b2",
}),
false,
);
});
test("findProviderConnection matches on provider (payload or meta)", () => {
const conns = [
{ id: "a", provider: "cloudflare_r2" },
{ id: "b", meta: { provider: "backblaze_b2" } },
];
assert.equal(findProviderConnection(conns, "backblaze_b2").id, "b");
assert.equal(findProviderConnection(conns, "generic_s3"), null);
assert.equal(connectionProvider(conns[1]), "backblaze_b2");
});
test("legacyDuplicateIds flags same-provider connections stored under a different id", () => {
const conns = [
{ id: "default", provider: "cloudflare_r2" }, // the legacy shared slot
{ id: "cloudflare_r2", provider: "cloudflare_r2" }, // the new per-provider slot
{ id: "backblaze_b2", provider: "backblaze_b2" },
];
assert.deepEqual(legacyDuplicateIds(conns, "cloudflare_r2", "cloudflare_r2"), ["default"]);
assert.deepEqual(legacyDuplicateIds(conns, "backblaze_b2", "backblaze_b2"), []);
});
});
// --- Integration against the real storageService (fake-indexeddb) ---
describe("saving multiple providers does not clash", () => {
const pubkey = "00".repeat(31) + "07";
const signer = {
nip44Encrypt: async (_pk, pt) => `enc_${pt}`,
nip44Decrypt: async (_pk, ct) => ct.replace(/^enc_/, ""),
};
beforeEach(async () => {
storageService.masterKeys.clear();
if (storageService.db) {
storageService.db.close();
storageService.db = null;
storageService.dbPromise = null;
}
await new Promise((resolve) => {
const req = globalThis.indexedDB.deleteDatabase("bitvid-storage");
req.onsuccess = resolve;
req.onerror = resolve;
req.onblocked = resolve;
});
await storageService.unlock(pubkey, { signer });
});
const r2Payload = { provider: "cloudflare_r2", accessKeyId: "r2k", secretAccessKey: "r2s", accountId: "acct" };
const b2Payload = { provider: "backblaze_b2", accessKeyId: "b2k", secretAccessKey: "b2s" };
test("R2 then B2 both persist under their own provider ids; only one is default", async () => {
await saveProviderConnection(storageService, pubkey, {
provider: "cloudflare_r2",
payload: r2Payload,
meta: { provider: "cloudflare_r2", bucket: "r2-bucket" },
isDefault: true,
});
await saveProviderConnection(storageService, pubkey, {
provider: "backblaze_b2",
payload: b2Payload,
meta: { provider: "backblaze_b2", bucket: "b2-bucket", region: "us-west-004" },
isDefault: false,
});
const conns = await storageService.listConnections(pubkey);
const ids = conns.map((c) => c.id).sort();
assert.deepEqual(ids, ["backblaze_b2", "cloudflare_r2"], "both providers stored, no overwrite");
const r2 = await storageService.getConnection(pubkey, "cloudflare_r2");
const b2 = await storageService.getConnection(pubkey, "backblaze_b2");
assert.equal(r2.meta.bucket, "r2-bucket");
assert.equal(b2.meta.bucket, "b2-bucket");
// Credentials are independent (B2 did not clobber R2).
assert.equal(r2.accessKeyId, "r2k");
assert.equal(b2.accessKeyId, "b2k");
// Exactly one default-for-uploads, and it's the one the user chose.
assert.equal(conns.filter((c) => c.meta?.defaultForUploads).length, 1);
assert.equal(r2.meta.defaultForUploads, true);
assert.equal(b2.meta.defaultForUploads, false);
});
test("re-saving B2 as the default moves the active target off R2 (still only one default)", async () => {
await saveProviderConnection(storageService, pubkey, { provider: "cloudflare_r2", payload: r2Payload, meta: { provider: "cloudflare_r2" }, isDefault: true });
await saveProviderConnection(storageService, pubkey, { provider: "backblaze_b2", payload: b2Payload, meta: { provider: "backblaze_b2" }, isDefault: false });
await saveProviderConnection(storageService, pubkey, { provider: "backblaze_b2", payload: b2Payload, meta: { provider: "backblaze_b2" }, isDefault: true });
const conns = await storageService.listConnections(pubkey);
assert.equal(conns.filter((c) => c.meta?.defaultForUploads).length, 1);
assert.equal((await storageService.getConnection(pubkey, "backblaze_b2")).meta.defaultForUploads, true);
assert.equal((await storageService.getConnection(pubkey, "cloudflare_r2")).meta.defaultForUploads, false);
});
test("a legacy shared 'default' slot is migrated to its per-provider id (no duplicate)", async () => {
// Simulate the old UI: an R2 connection under the shared "default" id.
await storageService.saveConnection(pubkey, "default", r2Payload, {
provider: "cloudflare_r2",
bucket: "legacy",
defaultForUploads: true,
});
// Re-saving R2 through the new path should re-key it and remove the legacy slot.
await saveProviderConnection(storageService, pubkey, {
provider: "cloudflare_r2",
payload: r2Payload,
meta: { provider: "cloudflare_r2", bucket: "legacy" },
isDefault: false,
});
const ids = (await storageService.listConnections(pubkey)).map((c) => c.id);
assert.ok(ids.includes("cloudflare_r2"));
assert.ok(!ids.includes("default"), "legacy 'default' slot removed");
// Default-for-uploads carried over from the migrated connection.
assert.equal((await storageService.getConnection(pubkey, "cloudflare_r2")).meta.defaultForUploads, true);
});
test("both connections are present in the exported account record (so sync carries them)", async () => {
await saveProviderConnection(storageService, pubkey, { provider: "cloudflare_r2", payload: r2Payload, meta: { provider: "cloudflare_r2" }, isDefault: true });
await saveProviderConnection(storageService, pubkey, { provider: "backblaze_b2", payload: b2Payload, meta: { provider: "backblaze_b2" }, isDefault: false });
const record = await storageService.exportAccountRecord(pubkey);
assert.deepEqual(
Object.keys(record.connections).sort(),
["backblaze_b2", "cloudflare_r2"],
"the synced record contains every provider's connection",
);
});
});