test(ai): add unit tests for storageService (agent)

This commit is contained in:
google-labs-jules[bot] committed 2026-01-22 11:37:02 +00:00
1 parent 870df0482a
commit ebd98dc106
1 file changed
+243
+243
View File
@@ -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!");