diff --git a/tests/nostr/sessionActor.test.mjs b/tests/nostr/sessionActor.test.mjs new file mode 100644 index 00000000..143e558c --- /dev/null +++ b/tests/nostr/sessionActor.test.mjs @@ -0,0 +1,120 @@ +import test from "node:test"; +import assert from "node:assert"; +import { + encryptSessionPrivateKey, + decryptSessionPrivateKey, + persistSessionActor, + readStoredSessionActorEntry, + clearStoredSessionActor, + SESSION_ACTOR_STORAGE_KEY, + __testExports +} from "../../js/nostr/sessionActor.js"; + +const { generateRandomBytes, isSubtleCryptoAvailable } = __testExports; + +// Ensure localStorage is mocked +import "../../tests/test-helpers/setup-localstorage.mjs"; + +test("js/nostr/sessionActor.js", async (t) => { + if (!isSubtleCryptoAvailable()) { + t.skip("WebCrypto not available in this environment"); + return; + } + + t.beforeEach(async () => { + localStorage.clear(); + await clearStoredSessionActor(); + }); + + await t.test("Encryption and Decryption Roundtrip", async () => { + const privateKey = "nsec1testkey..."; // In reality this would be hex or bech32, but for encryption it is just a string payload + const passphrase = "secure-password"; + + const encrypted = await encryptSessionPrivateKey(privateKey, passphrase); + + assert.ok(encrypted.ciphertext); + assert.ok(encrypted.salt); + assert.ok(encrypted.iv); + assert.strictEqual(encrypted.algorithm, "AES-GCM"); + + const payload = { + privateKeyEncrypted: encrypted.ciphertext, + encryption: encrypted, + }; + + const decrypted = await decryptSessionPrivateKey(payload, passphrase); + assert.strictEqual(decrypted, privateKey); + }); + + await t.test("Decryption fails with wrong passphrase", async () => { + const privateKey = "secret"; + const passphrase = "correct"; + const wrongPassphrase = "wrong"; + + const encrypted = await encryptSessionPrivateKey(privateKey, passphrase); + const payload = { + privateKeyEncrypted: encrypted.ciphertext, + encryption: encrypted, + }; + + await assert.rejects( + async () => { + await decryptSessionPrivateKey(payload, wrongPassphrase); + }, + (err) => err.code === "decrypt-failed" || err.message.includes("decrypt-failed") + ); + }); + + await t.test("Persistence and Retrieval", async () => { + const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; + const privateKey = "my-secret-key"; + const passphrase = "my-password"; + + const encrypted = await encryptSessionPrivateKey(privateKey, passphrase); + + const actor = { + pubkey, + privateKeyEncrypted: encrypted.ciphertext, + encryption: encrypted, + createdAt: Date.now(), + }; + + persistSessionActor(actor); + + // Verify localStorage + const storedRaw = localStorage.getItem(SESSION_ACTOR_STORAGE_KEY); + assert.ok(storedRaw); + + const stored = readStoredSessionActorEntry(); + assert.ok(stored); + assert.strictEqual(stored.pubkey, pubkey); + assert.strictEqual(stored.privateKeyEncrypted, encrypted.ciphertext); + + // Verify we can decrypt what we retrieved + const decrypted = await decryptSessionPrivateKey(stored, passphrase); + assert.strictEqual(decrypted, privateKey); + }); + + await t.test("Clear Stored Session Actor", async () => { + const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; + const actor = { + pubkey, + privateKeyEncrypted: "fake-encrypted-data", + encryption: { + salt: "fake-salt", + iv: "fake-iv", + iterations: 1000, + hash: "SHA-256", + algorithm: "AES-GCM", + version: 1 + }, + createdAt: Date.now(), + }; + + persistSessionActor(actor); + assert.ok(readStoredSessionActorEntry()); + + clearStoredSessionActor(); + assert.strictEqual(readStoredSessionActorEntry(), null); + }); +}); diff --git a/tests/services/r2Service.test.mjs b/tests/services/r2Service.test.mjs new file mode 100644 index 00000000..c92654af --- /dev/null +++ b/tests/services/r2Service.test.mjs @@ -0,0 +1,106 @@ +import test from "node:test"; +import assert from "node:assert"; +import r2Service from "../../js/services/r2Service.js"; + +// Ensure localStorage is mocked +import "../../tests/test-helpers/setup-localstorage.mjs"; + +test("js/services/r2Service.js", async (t) => { + t.beforeEach(async () => { + localStorage.clear(); + await r2Service.clearSettings(); + }); + + await t.test("Default Settings", () => { + const settings = r2Service.getSettings(); + // The actual default settings might contain more fields than we checked. + // Let's verify the core fields match. + assert.strictEqual(settings.accountId, ""); + assert.strictEqual(settings.accessKeyId, ""); + assert.strictEqual(settings.secretAccessKey, ""); + assert.strictEqual(settings.baseDomain, ""); + assert.deepStrictEqual(settings.buckets, {}); + }); + + await t.test("handleCloudflareSettingsSubmit - Valid Input", async () => { + const input = { + accountId: "acc123", + accessKeyId: "key123", + secretAccessKey: "secret123", + baseDomain: "https://pub-xxx.r2.dev", + }; + + const result = await r2Service.handleCloudflareSettingsSubmit(input); + assert.strictEqual(result, true); + + const saved = r2Service.getSettings(); + assert.strictEqual(saved.accountId, input.accountId); + assert.strictEqual(saved.accessKeyId, input.accessKeyId); + assert.strictEqual(saved.secretAccessKey, input.secretAccessKey); + assert.strictEqual(saved.baseDomain, input.baseDomain); + }); + + await t.test("handleCloudflareSettingsSubmit - Invalid S3 URL", async () => { + const input = { + accountId: "acc123", + accessKeyId: "key123", + secretAccessKey: "secret123", + baseDomain: "https://something.r2.cloudflarestorage.com", + }; + + let status = ""; + const cleanup = r2Service.on("settingsStatus", (s) => (status = s.message)); + + const result = await r2Service.handleCloudflareSettingsSubmit(input); + assert.strictEqual(result, false); + assert.match(status, /entered the S3 API URL/); + + cleanup(); + }); + + await t.test("handleCloudflareSettingsSubmit - Missing Fields", async () => { + const input = { + accountId: "acc123", + // missing accessKeyId + secretAccessKey: "secret123", + baseDomain: "https://pub-xxx.r2.dev", + }; + + const result = await r2Service.handleCloudflareSettingsSubmit(input); + assert.strictEqual(result, false); + }); + + await t.test("ensureBucketConfigForNpub - Uses Explicit Credentials", async () => { + const npub = "npub1test"; + const credentials = { + accountId: "acc123", + accessKeyId: "key123", + secretAccessKey: "secret123", + baseDomain: "https://pub-xxx.r2.dev", + bucket: "custom-bucket", + isLegacy: true, + }; + + const result = await r2Service.ensureBucketConfigForNpub(npub, { credentials }); + assert.strictEqual(result.entry.bucket, "custom-bucket"); + assert.strictEqual(result.entry.publicBaseUrl, "https://pub-xxx.r2.dev"); + assert.strictEqual(result.customDomainStatus, "manual"); + }); + + await t.test("resolveConnection - Returns Legacy Settings if no StorageService", async () => { + const input = { + accountId: "legacyAcc", + accessKeyId: "legacyKey", + secretAccessKey: "legacySecret", + baseDomain: "https://legacy.com", + }; + await r2Service.handleCloudflareSettingsSubmit(input); + + const npub = "npub1any"; + const resolved = await r2Service.resolveConnection(npub); + + assert.ok(resolved); + assert.strictEqual(resolved.accountId, "legacyAcc"); + assert.strictEqual(resolved.isLegacy, true); + }); +}); diff --git a/tests/state/cache.test.mjs b/tests/state/cache.test.mjs new file mode 100644 index 00000000..c21a6684 --- /dev/null +++ b/tests/state/cache.test.mjs @@ -0,0 +1,105 @@ +import test from "node:test"; +import assert from "node:assert"; +import { + loadSavedProfilesFromStorage, + persistSavedProfiles, + setActiveProfilePubkey, + getSavedProfiles, + getActiveProfilePubkey, + setSavedProfiles, + storeUrlHealth, + getCachedUrlHealth, + getModerationSettings, + setModerationSettings, + urlHealthConstants, +} from "../../js/state/cache.js"; + +// Ensure localStorage is mocked +import "../../tests/test-helpers/setup-localstorage.mjs"; + +test("js/state/cache.js", async (t) => { + t.beforeEach(() => { + localStorage.clear(); + // Reset internal state if possible, or relying on overwrites + setSavedProfiles([]); + setActiveProfilePubkey(null, { persist: false }); + }); + + await t.test("Saved Profiles Persistence", async () => { + const profiles = [ + { pubkey: "0000000000000000000000000000000000000000000000000000000000000001", name: "Alice" }, + { pubkey: "0000000000000000000000000000000000000000000000000000000000000002", name: "Bob" }, + ]; + + setSavedProfiles(profiles, { persist: true }); + + // Simulate reload + const { profiles: loadedProfiles } = loadSavedProfilesFromStorage(); + assert.strictEqual(loadedProfiles.length, 2); + assert.strictEqual(loadedProfiles[0].name, "Alice"); + assert.strictEqual(loadedProfiles[1].name, "Bob"); + }); + + await t.test("Active Profile Pubkey", async () => { + const pubkey = "0000000000000000000000000000000000000000000000000000000000000001"; + // We need to set saved profiles first so the active one is considered valid in some logic paths, + // or just rely on setActiveProfilePubkey which should handle it. + // However, looking at cache.js `loadSavedProfilesFromStorage` implementation: + // It checks if the active candidate exists in `seenPubkeys` (from `savedProfiles`). + // If not, it might not set it or treat it as valid if we are loading from storage. + + // Let's add the profile to saved profiles first to be safe for persistence reload check. + setSavedProfiles([{ pubkey, name: "Alice" }]); + + setActiveProfilePubkey(pubkey); + + assert.strictEqual(getActiveProfilePubkey(), pubkey); + + // Verify persistence + const { activePubkey } = loadSavedProfilesFromStorage(); + assert.strictEqual(activePubkey, pubkey); + }); + + await t.test("URL Health Caching", async () => { + const eventId = "event1"; + const url = "https://example.com/video.mp4"; + const result = { status: "ok", message: "All good" }; + + storeUrlHealth(eventId, url, result); + + const cached = getCachedUrlHealth(eventId, url); + assert.ok(cached); + assert.strictEqual(cached.status, "ok"); + assert.strictEqual(cached.url, url); + }); + + await t.test("URL Health Expiration", async (t) => { + const eventId = "event2"; + const url = "https://example.com/expired.mp4"; + const result = { status: "checking" }; + + // Store with very short TTL + storeUrlHealth(eventId, url, result, 1); // 1ms TTL + + await new Promise((resolve) => setTimeout(resolve, 10)); + + const cached = getCachedUrlHealth(eventId, url); + assert.strictEqual(cached, null, "Should return null for expired cache"); + }); + + await t.test("Moderation Settings", async () => { + const defaults = getModerationSettings(); + assert.ok(defaults); + + const newSettings = { + blurThreshold: 1, + autoplayBlockThreshold: 2, + }; + + setModerationSettings(newSettings); + + const updated = getModerationSettings(); + assert.strictEqual(updated.blurThreshold, 1); + assert.strictEqual(updated.autoplayBlockThreshold, 2); + }); +});