From fa10f943e115d8f69a9b67d255774c025652e56f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:44:16 +0000 Subject: [PATCH] test(ai): add unit tests for storageService and appState (agent) --- tests/app-state.test.mjs | 108 ++++++++++++++++++++++ tests/storage-service.test.mjs | 158 +++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 tests/app-state.test.mjs create mode 100644 tests/storage-service.test.mjs diff --git a/tests/app-state.test.mjs b/tests/app-state.test.mjs new file mode 100644 index 00000000..11dee51f --- /dev/null +++ b/tests/app-state.test.mjs @@ -0,0 +1,108 @@ +import { test, describe, beforeEach } from "node:test"; +import assert from "node:assert"; + +// Import module under test +import * as appState from "../js/state/appState.js"; + +describe("AppState", () => { + beforeEach(() => { + appState.resetAppState(); + }); + + test("Initial state is clean", () => { + const state = appState.getAppState(); + assert.strictEqual(state.pubkey, null); + assert.strictEqual(state.currentUserNpub, null); + assert.strictEqual(state.currentVideo, null); + assert.strictEqual(state.videosMap, null); + assert.strictEqual(state.videoSubscription, null); + assert.deepStrictEqual(state.modals, {}); + }); + + test("setPubkey() updates state and notifies subscribers", (t) => { + let callCount = 0; + const cleanup = appState.subscribeToAppStateKey("pubkey", (val, prev, snap) => { + callCount++; + assert.strictEqual(val, "new_pubkey"); + assert.strictEqual(prev, null); + assert.strictEqual(snap.pubkey, "new_pubkey"); + }); + t.after(cleanup); + + appState.setPubkey("new_pubkey"); + assert.strictEqual(appState.getPubkey(), "new_pubkey"); + assert.strictEqual(callCount, 1); + }); + + test("setCurrentUserNpub() updates state", () => { + appState.setCurrentUserNpub("npub123"); + assert.strictEqual(appState.getCurrentUserNpub(), "npub123"); + }); + + test("setCurrentVideo() updates state", () => { + const video = { id: "v1" }; + appState.setCurrentVideo(video); + assert.strictEqual(appState.getCurrentVideo(), video); + }); + + test("setVideosMap() updates state", () => { + const map = new Map([["v1", {}]]); + appState.setVideosMap(map); + assert.strictEqual(appState.getVideosMap(), map); + }); + + test("setVideoSubscription() updates state", () => { + const sub = { id: "sub1" }; + appState.setVideoSubscription(sub); + assert.strictEqual(appState.getVideoSubscription(), sub); + }); + + test("setModalState() updates state and notifies modal subscribers", (t) => { + let modalCallCount = 0; + const cleanup = appState.subscribeToModalState("login", (val, prev) => { + modalCallCount++; + assert.strictEqual(val, true); + assert.strictEqual(prev, false); + }); + t.after(cleanup); + + appState.setModalState("login", true); + assert.strictEqual(appState.getModalState("login"), true); + assert.strictEqual(modalCallCount, 1); + + // Test no change behavior + appState.setModalState("login", true); + assert.strictEqual(modalCallCount, 1); // Should not increase + }); + + test("Global subscriber receives updates", (t) => { + let callCount = 0; + const cleanup = appState.subscribeToAppState((snapshot, change) => { + callCount++; + assert.strictEqual(change.key, "pubkey"); + assert.strictEqual(change.value, "pk1"); + assert.strictEqual(snapshot.pubkey, "pk1"); + }); + t.after(cleanup); + + appState.setPubkey("pk1"); + assert.strictEqual(callCount, 1); + }); + + test("resetAppState() clears all state and notifies", (t) => { + appState.setPubkey("pk1"); + appState.setModalState("login", true); + + let callCount = 0; + const cleanup = appState.subscribeToAppStateKey("pubkey", (val) => { + if (val === null) callCount++; + }); + t.after(cleanup); + + appState.resetAppState(); + + assert.strictEqual(appState.getPubkey(), null); + assert.strictEqual(appState.getModalState("login"), false); + assert.strictEqual(callCount, 1); + }); +}); diff --git a/tests/storage-service.test.mjs b/tests/storage-service.test.mjs new file mode 100644 index 00000000..27ef3e9f --- /dev/null +++ b/tests/storage-service.test.mjs @@ -0,0 +1,158 @@ +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); + }); +});