mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
244 lines
7.9 KiB
JavaScript
244 lines
7.9 KiB
JavaScript
// 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!");
|