Files
bitvid/tests/storage-service.test.mjs
T

159 lines
5.7 KiB
JavaScript

import { test, mock, describe, beforeEach } from "node:test";
import assert from "node:assert";
import * as fakeIndexedDB from "fake-indexeddb";
// Ensure global indexedDB is set
if (!globalThis.indexedDB) {
globalThis.indexedDB = fakeIndexedDB.indexedDB;
globalThis.IDBKeyRange = fakeIndexedDB.IDBKeyRange;
}
// Import module under test
import storageService, { PROVIDERS } from "../js/services/storageService.js";
describe("StorageService", () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
const mockSigner = {
nip44Encrypt: mock.fn(async (pk, plaintext) => `encrypted_${plaintext}`),
nip44Decrypt: mock.fn(async (pk, ciphertext) => ciphertext.replace("encrypted_", "")),
};
const mockLegacySigner = {
nip04Encrypt: mock.fn(async (pk, plaintext) => `legacy_encrypted_${plaintext}`),
nip04Decrypt: mock.fn(async (pk, ciphertext) => ciphertext.replace("legacy_encrypted_", "")),
};
beforeEach(async () => {
// Reset service state
storageService.masterKeys.clear();
// Clear IndexedDB
if (storageService.db) {
storageService.db.close();
storageService.db = null;
storageService.dbPromise = null;
}
const req = globalThis.indexedDB.deleteDatabase("bitvid-storage");
await new Promise((resolve, reject) => {
req.onsuccess = resolve;
req.onerror = resolve; // ignore error if db doesn't exist
req.onblocked = resolve;
});
});
test("init() creates database and object store", async () => {
const db = await storageService.init();
assert.ok(db);
assert.strictEqual(db.name, "bitvid-storage");
assert.ok(db.objectStoreNames.contains("accounts"));
});
test("unlock() generates and stores master key with NIP-44", async () => {
await storageService.unlock(pubkey, { signer: mockSigner });
assert.ok(storageService.isUnlocked(pubkey));
assert.strictEqual(mockSigner.nip44Encrypt.mock.callCount(), 1);
// Verify persistence
const account = await storageService._getAccount(pubkey);
assert.ok(account);
assert.strictEqual(account.encryptedMasterKey.method, "nip44");
});
test("unlock() restores existing master key", async () => {
// First unlock to create
await storageService.unlock(pubkey, { signer: mockSigner });
storageService.lock(pubkey);
assert.strictEqual(storageService.isUnlocked(pubkey), false);
// Second unlock to restore
await storageService.unlock(pubkey, { signer: mockSigner });
assert.ok(storageService.isUnlocked(pubkey));
assert.strictEqual(mockSigner.nip44Decrypt.mock.callCount(), 1);
});
test("unlock() falls back to NIP-04 if NIP-44 unavailable", async () => {
await storageService.unlock(pubkey, { signer: mockLegacySigner });
const account = await storageService._getAccount(pubkey);
assert.strictEqual(account.encryptedMasterKey.method, "nip04");
assert.strictEqual(mockLegacySigner.nip04Encrypt.mock.callCount(), 1);
});
test("saveConnection() encrypts and stores connection", async () => {
await storageService.unlock(pubkey, { signer: mockSigner });
const connectionId = "conn_1";
const payload = {
accessKeyId: "key",
secretAccessKey: "secret",
provider: PROVIDERS.R2,
};
const meta = { label: "My R2" };
await storageService.saveConnection(pubkey, connectionId, payload, meta);
const connections = await storageService.listConnections(pubkey);
assert.strictEqual(connections.length, 1);
assert.strictEqual(connections[0].id, connectionId);
assert.strictEqual(connections[0].meta.label, "My R2");
// Verify payload is encrypted in DB
const account = await storageService._getAccount(pubkey);
const storedConn = account.connections[connectionId];
assert.ok(storedConn.encrypted.cipher);
// Ensure it's not plaintext
assert.notStrictEqual(storedConn.encrypted.cipher, JSON.stringify(payload));
});
test("getConnection() decrypts and returns connection", async () => {
await storageService.unlock(pubkey, { signer: mockSigner });
const connectionId = "conn_1";
const payload = {
accessKeyId: "key",
secretAccessKey: "secret",
provider: PROVIDERS.R2,
};
await storageService.saveConnection(pubkey, connectionId, payload);
const retrieved = await storageService.getConnection(pubkey, connectionId);
assert.strictEqual(retrieved.accessKeyId, payload.accessKeyId);
assert.strictEqual(retrieved.secretAccessKey, payload.secretAccessKey);
});
test("getConnection() throws if locked", async () => {
await assert.rejects(
async () => await storageService.getConnection(pubkey, "conn_1"),
/Storage is locked/
);
});
test("deleteConnection() removes connection", async () => {
await storageService.unlock(pubkey, { signer: mockSigner });
await storageService.saveConnection(pubkey, "conn_1", { provider: PROVIDERS.R2 });
await storageService.deleteConnection(pubkey, "conn_1");
const connections = await storageService.listConnections(pubkey);
assert.strictEqual(connections.length, 0);
});
test("setDefaultConnection() updates metadata", async () => {
await storageService.unlock(pubkey, { signer: mockSigner });
await storageService.saveConnection(pubkey, "conn_1", { provider: PROVIDERS.R2 });
await storageService.saveConnection(pubkey, "conn_2", { provider: PROVIDERS.S3 });
await storageService.setDefaultConnection(pubkey, "conn_2");
const conns = await storageService.listConnections(pubkey);
const c1 = conns.find(c => c.id === "conn_1");
const c2 = conns.find(c => c.id === "conn_2");
assert.strictEqual(c1.meta.defaultForUploads, false);
assert.strictEqual(c2.meta.defaultForUploads, true);
});
});