mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
test(ai): add unit tests for storageService (agent)
This commit is contained in:
1 parent
870df0482a
commit
ebd98dc106
1 file changed
+243
@@ -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!");
|
||||
Reference in new issue
Block a user