From ebd98dc106bfef701023c881bb878aab3d876899 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:37:02 +0000 Subject: [PATCH] test(ai): add unit tests for storageService (agent) --- tests/services/storage-service.test.mjs | 243 ++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 tests/services/storage-service.test.mjs diff --git a/tests/services/storage-service.test.mjs b/tests/services/storage-service.test.mjs new file mode 100644 index 00000000..39197026 --- /dev/null +++ b/tests/services/storage-service.test.mjs @@ -0,0 +1,243 @@ +// Run with: node tests/services/storage-service.test.mjs + +import "../test-helpers/setup-localstorage.mjs"; +import "fake-indexeddb/auto"; +import assert from "node:assert/strict"; + +// Fix for fake-indexeddb in Node env with window mock: +// fake-indexeddb might attach to window.indexedDB because window exists, +// but in Node, window is not the global scope, so 'indexedDB' global lookup fails. +if (typeof globalThis.indexedDB === "undefined" && globalThis.window?.indexedDB) { + globalThis.indexedDB = globalThis.window.indexedDB; + globalThis.IDBKeyRange = globalThis.window.IDBKeyRange; +} +import { StorageService, PROVIDERS } from "../../js/services/storageService.js"; + +// Ensure global crypto is available (Node.js 19+) +if (!globalThis.crypto) { + throw new Error("Crypto API is not available in this environment."); +} + +// Mock userLogger to avoid console noise during tests +// We can't easily mock the module import, so we'll just suppress console output if needed. +// For now, we'll let it log, as it helps debugging. + +const SAMPLE_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001"; + +// Helper to create a mock signer +function createMockSigner() { + return { + nip04Encrypt: async (pubkey, plaintext) => { + return `nip04_enc:${plaintext}`; + }, + nip04Decrypt: async (pubkey, ciphertext) => { + if (!ciphertext.startsWith("nip04_enc:")) { + throw new Error("Decryption failed"); + } + return ciphertext.replace("nip04_enc:", ""); + }, + nip44Encrypt: async (pubkey, plaintext) => { + return `nip44_enc:${plaintext}`; + }, + nip44Decrypt: async (pubkey, ciphertext) => { + if (!ciphertext.startsWith("nip44_enc:")) { + throw new Error("Decryption failed"); + } + return ciphertext.replace("nip44_enc:", ""); + } + }; +} + +// Helper: Delete DB to ensure clean state +async function deleteDatabase() { + const idb = globalThis.indexedDB || globalThis.window?.indexedDB; + if (!idb) { + throw new Error("IndexedDB is not available in global scope"); + } + return new Promise((resolve, reject) => { + const request = idb.deleteDatabase("bitvid-storage"); + request.onsuccess = resolve; + request.onerror = reject; + request.onblocked = resolve; // Just proceed + }); +} + +// Tests + +// 1. Initialization +await (async () => { + console.log("Test: Initialization"); + await deleteDatabase(); + const storage = new StorageService(); + const db = await storage.init(); + assert.ok(db, "Database should be initialized"); + assert.equal(db.name, "bitvid-storage"); + assert.ok(db.objectStoreNames.contains("accounts"), "Should contain 'accounts' store"); + db.close(); +})(); + +// 2. Unlock with NIP-04 (Legacy) +await (async () => { + console.log("Test: Unlock with NIP-04"); + await deleteDatabase(); + const storage = new StorageService(); + const signer = createMockSigner(); + + try { + // Remove nip44 methods to force NIP-04 + const legacySigner = { + nip04Encrypt: signer.nip04Encrypt, + nip04Decrypt: signer.nip04Decrypt + }; + + await storage.unlock(SAMPLE_PUBKEY, { signer: legacySigner }); + assert.ok(storage.isUnlocked(SAMPLE_PUBKEY), "Storage should be unlocked"); + + // Verify account created + const account = await storage._getAccount(SAMPLE_PUBKEY); + assert.ok(account, "Account should be created"); + assert.equal(account.pubkey, SAMPLE_PUBKEY); + assert.equal(account.encryptedMasterKey.method, "nip04"); + assert.ok(account.encryptedMasterKey.ciphertext.startsWith("nip04_enc:"), "Master key should be encrypted with NIP-04"); + } finally { + if (storage.db) storage.db.close(); + } +})(); + +// 3. Unlock with NIP-44 +await (async () => { + console.log("Test: Unlock with NIP-44"); + await deleteDatabase(); + const storage = new StorageService(); + const signer = createMockSigner(); + + try { + await storage.unlock(SAMPLE_PUBKEY, { signer }); // Has nip44 methods + assert.ok(storage.isUnlocked(SAMPLE_PUBKEY)); + + const account = await storage._getAccount(SAMPLE_PUBKEY); + assert.equal(account.encryptedMasterKey.method, "nip44"); + assert.ok(account.encryptedMasterKey.ciphertext.startsWith("nip44_enc:"), "Master key should be encrypted with NIP-44"); + } finally { + if (storage.db) storage.db.close(); + } +})(); + +// 4. Save and Get Connection (Encryption/Decryption) +await (async () => { + console.log("Test: Save and Get Connection"); + await deleteDatabase(); + const storage = new StorageService(); + const signer = createMockSigner(); + + try { + await storage.unlock(SAMPLE_PUBKEY, { signer }); + + const connectionId = "conn_1"; + const payload = { + provider: PROVIDERS.R2, + accessKeyId: "TEST_ACCESS_KEY", + secretAccessKey: "TEST_SECRET_KEY", + endpoint: "https://example.com" + }; + const meta = { + label: "My R2 Bucket" + }; + + await storage.saveConnection(SAMPLE_PUBKEY, connectionId, payload, meta); + + // Verify raw storage is encrypted + const account = await storage._getAccount(SAMPLE_PUBKEY); + const connStored = account.connections[connectionId]; + assert.ok(connStored.encrypted.cipher, "Payload should be encrypted"); + assert.notEqual(connStored.encrypted.cipher, JSON.stringify(payload), "Cipher should not match plaintext"); + + // Retrieve and decrypt + const retrieved = await storage.getConnection(SAMPLE_PUBKEY, connectionId); + assert.deepEqual(retrieved.accessKeyId, payload.accessKeyId); + assert.deepEqual(retrieved.secretAccessKey, payload.secretAccessKey); + assert.equal(retrieved.meta.label, meta.label); + } finally { + if (storage.db) storage.db.close(); + } +})(); + +// 5. Locking +await (async () => { + console.log("Test: Locking"); + await deleteDatabase(); + const storage = new StorageService(); + const signer = createMockSigner(); + + try { + await storage.unlock(SAMPLE_PUBKEY, { signer }); + assert.ok(storage.isUnlocked(SAMPLE_PUBKEY)); + + storage.lock(SAMPLE_PUBKEY); + assert.ok(!storage.isUnlocked(SAMPLE_PUBKEY)); + + // Try to access without unlock + await assert.rejects( + async () => await storage.getConnection(SAMPLE_PUBKEY, "conn_1"), + /Storage is locked/, + "Should reject access when locked" + ); + } finally { + if (storage.db) storage.db.close(); + } +})(); + +// 6. List and Delete Connections +await (async () => { + console.log("Test: List and Delete Connections"); + await deleteDatabase(); + const storage = new StorageService(); + const signer = createMockSigner(); + + try { + await storage.unlock(SAMPLE_PUBKEY, { signer }); + + await storage.saveConnection(SAMPLE_PUBKEY, "c1", { provider: "p1" }, { label: "C1" }); + await storage.saveConnection(SAMPLE_PUBKEY, "c2", { provider: "p2" }, { label: "C2" }); + + const list = await storage.listConnections(SAMPLE_PUBKEY); + assert.equal(list.length, 2); + assert.ok(list.find(c => c.id === "c1")); + assert.ok(list.find(c => c.id === "c2")); + + await storage.deleteConnection(SAMPLE_PUBKEY, "c1"); + const listAfter = await storage.listConnections(SAMPLE_PUBKEY); + assert.equal(listAfter.length, 1); + assert.equal(listAfter[0].id, "c2"); + } finally { + if (storage.db) storage.db.close(); + } +})(); + +// 7. Relock and Unlock (Persistence) +await (async () => { + console.log("Test: Persistence (Unlock -> Lock -> Unlock)"); + await deleteDatabase(); + const storage = new StorageService(); + const signer = createMockSigner(); + + let storage2; + try { + // Initial unlock and save + await storage.unlock(SAMPLE_PUBKEY, { signer }); + await storage.saveConnection(SAMPLE_PUBKEY, "c1", { secret: "secret123" }, { label: "Persistent" }); + storage.lock(SAMPLE_PUBKEY); + + // Re-unlock + storage2 = new StorageService(); // Simulate app restart or new service instance + await storage2.unlock(SAMPLE_PUBKEY, { signer }); + + const conn = await storage2.getConnection(SAMPLE_PUBKEY, "c1"); + assert.equal(conn.secret, "secret123", "Should be able to decrypt data persisted in DB"); + } finally { + if (storage.db) storage.db.close(); + if (storage2 && storage2.db) storage2.db.close(); + } +})(); + +console.log("All tests passed!");