Files
bitvid/tests/nostr/sessionActor.test.mjs
T
thePR0M3TH3ANandClaude Fable 5 99d1b1dd36 tests: spec-correct four stale tests hidden by piped exit codes
A per-file sweep (local suite runs were reading the PIPE's exit code, not
npm's) surfaced four tests failing on HEAD:

- hash-change-handler: logged-out default is deliberately TRENDING since
  04621efc; test still expected most-recent-videos
- session-actor / sessionActor / nsec-login: encoded the pre-fix "no-arg
  clearStoredSessionActor wipes EVERY account" behavior that 2026-07-02
  deliberately removed (one account's logout must not destroy other saved
  accounts' keys). Replaced with stronger assertions: multi-account survival
  on no-arg clear, targeted forget, and the blocked-key security property
  asserted on the blocked account specifically.

Integrity notes in each file; TODO #11b addendum documents the pipe lesson.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 21:01:45 -04:00

155 lines
6.0 KiB
JavaScript

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);
});
// spec_correction: no-arg clearStoredSessionActor() used to wipe the WHOLE
// per-account map — the data-loss bug where logging out of one account
// silently destroyed every other saved account's remembered nsec key. The
// deliberate fix (2026-07-02, same class as the NIP-46 session-map wipe)
// made no-arg clear ONLY the legacy v1 "last saved" slot; forgetting a
// specific account is clearStoredSessionActor(pubkey). This test encoded the
// dangerous pre-fix behavior; it now asserts the fixed semantics, including
// the multi-account survival property the fix exists for.
//
// test_integrity_note:
// change_type: ["spec_correction"]
// scenarios:
// - id: SCN-session-actor-targeted-clear
// given: "two accounts with persisted encrypted keys"
// when: "no-arg clear runs (logout-ish) and then a targeted clear"
// then: "no-arg leaves BOTH accounts retrievable by pubkey; targeted clear removes exactly one"
// observable_outcomes:
// - "clearStoredSessionActor() never deletes other accounts' keys"
// - "clearStoredSessionActor(pubkey) removes that account's entry"
// determinism_controls:
// - "in-memory localStorage; fixed pubkeys"
// anti_cheat_rationale:
// prevents: ["snapshot rubber-stamping"]
// relaxation:
// did_relax_any_assertion: false
// if_true_explain_spec_basis: "replaced with the fixed (stronger, multi-account) semantics"
await t.test("Clear semantics: targeted forget, no-arg never nukes other accounts", async () => {
const pubkeyA = "0000000000000000000000000000000000000000000000000000000000000001";
const pubkeyB = "0000000000000000000000000000000000000000000000000000000000000002";
const encryption = {
salt: "fake-salt",
iv: "fake-iv",
iterations: 1000,
hash: "SHA-256",
algorithm: "AES-GCM",
version: 1
};
persistSessionActor({ pubkey: pubkeyA, privateKeyEncrypted: "cipher-a", encryption, createdAt: Date.now() });
persistSessionActor({ pubkey: pubkeyB, privateKeyEncrypted: "cipher-b", encryption, createdAt: Date.now() });
// No-arg clear (the logout-path call) drops only the "last saved" default…
clearStoredSessionActor();
assert.strictEqual(localStorage.getItem(SESSION_ACTOR_STORAGE_KEY), null);
// …while BOTH accounts' keys survive and stay switchable.
assert.ok(readStoredSessionActorEntry(pubkeyA), "account A must survive a no-arg clear");
assert.ok(readStoredSessionActorEntry(pubkeyB), "account B must survive a no-arg clear");
// Targeted clear forgets exactly that account.
clearStoredSessionActor(pubkeyA);
assert.strictEqual(readStoredSessionActorEntry(pubkeyA), null, "targeted clear removes the account");
assert.ok(readStoredSessionActorEntry(pubkeyB), "the other account is untouched");
clearStoredSessionActor(pubkeyB);
assert.strictEqual(readStoredSessionActorEntry(pubkeyB), null);
assert.strictEqual(readStoredSessionActorEntry(), null, "nothing left after every account is forgotten");
});
});