Files
bitvid/tests/settings-restore-prompt.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 4186b4c4ba feat(sync): offer-to-pull encrypted settings on login (one-time, non-blocking)
Completes todo #15. On login, if an encrypted copy of the user's storage and/or
wallet settings exists on their Nostr account — and this device hasn't already
opted in — offer to restore it.

- js/nostr/encryptedSync.js: exists(dTag) — cheap list-only existence peek (NO
  decrypt, so it doesn't spend the signer budget); decryption only on accept.
- storage/walletSyncService: hasRemote() wrappers over exists().
- js/services/settingsRestorePrompt.js: one-time per-pubkey offer. Only prompts
  for items with a remote copy not already enabled locally; if nothing remote
  exists it does NOT consume the one-time offer (a note published later can still
  prompt next login). On accept, pulls + applies.
- app.js handleLoginModalSuccess: fires maybeOfferSettingsRestore(pubkey) after
  login, deferred ~2s so relays connect; best-effort, never blocks login; toasts
  on restore.

tests/settings-restore-prompt.test.mjs: offer+restore-only-remote-items, decline,
prompt-once, skip-already-enabled, don't-consume-when-empty. Mutation-verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 18:08:43 -04:00

129 lines
4.3 KiB
JavaScript

// Offer-to-pull on login (todo #15): after login, if an encrypted copy exists on
// the user's account and this device hasn't opted in, offer to restore — once.
// Scenarios assert observable outcomes:
// - prompts and restores only the items that have a remote copy + are not
// already enabled locally
// - declining does not restore
// - it never nags twice once it has prompted (per pubkey)
// - if nothing remote exists, it does NOT consume the one-time offer
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
import { createSettingsRestorePrompt } from "../js/services/settingsRestorePrompt.js";
const PUBKEY = "c".repeat(64);
function makeSyncStub({ available = true, enabled = false, remote = false } = {}) {
return {
pulls: 0,
isAvailable: () => available,
isEnabled: () => enabled,
hasRemote: async () => remote,
pull: async function () {
this.pulls += 1;
return { found: true, imported: true };
},
};
}
test("offers and restores items that have a remote copy and aren't already enabled", async () => {
localStorage.clear();
const storageSync = makeSyncStub({ remote: true });
const walletSync = makeSyncStub({ remote: false }); // no wallet note
const restoredCalls = [];
const prompt = createSettingsRestorePrompt({
storageSync,
walletSync,
confirm: () => true,
});
const result = await prompt.maybeOffer(PUBKEY, {
onRestored: (items) => restoredCalls.push(items),
});
assert.equal(result.offered, true);
assert.equal(result.accepted, true);
assert.deepEqual(result.restored, ["storage"]);
assert.equal(storageSync.pulls, 1, "storage (has remote) must be pulled");
assert.equal(walletSync.pulls, 0, "wallet (no remote) must NOT be pulled");
assert.deepEqual(restoredCalls, [["storage"]]);
});
test("declining the prompt restores nothing", async () => {
localStorage.clear();
const storageSync = makeSyncStub({ remote: true });
const prompt = createSettingsRestorePrompt({
storageSync,
walletSync: makeSyncStub({ remote: false }),
confirm: () => false,
});
const result = await prompt.maybeOffer(PUBKEY);
assert.equal(result.offered, true);
assert.equal(result.accepted, false);
assert.equal(storageSync.pulls, 0, "decline must not pull");
});
test("does not offer twice once it has prompted (per pubkey)", async () => {
localStorage.clear();
let confirmCount = 0;
const prompt = createSettingsRestorePrompt({
storageSync: makeSyncStub({ remote: true }),
walletSync: makeSyncStub({ remote: false }),
confirm: () => {
confirmCount += 1;
return false;
},
});
await prompt.maybeOffer(PUBKEY);
const second = await prompt.maybeOffer(PUBKEY);
assert.equal(confirmCount, 1, "must prompt only once");
assert.equal(second.offered, false);
assert.equal(second.reason, "already-offered");
});
test("skips items already enabled on this device", async () => {
localStorage.clear();
const storageSync = makeSyncStub({ remote: true, enabled: true }); // already opted in here
const walletSync = makeSyncStub({ remote: true, enabled: false });
const prompt = createSettingsRestorePrompt({
storageSync,
walletSync,
confirm: () => true,
});
const result = await prompt.maybeOffer(PUBKEY);
assert.deepEqual(result.restored, ["wallet"], "only the not-yet-enabled item is offered");
assert.equal(storageSync.pulls, 0);
assert.equal(walletSync.pulls, 1);
});
test("nothing remote: does NOT consume the one-time offer (can prompt later)", async () => {
localStorage.clear();
const storageSync = makeSyncStub({ remote: false });
const walletSync = makeSyncStub({ remote: false });
let confirmCount = 0;
const prompt = createSettingsRestorePrompt({
storageSync,
walletSync,
confirm: () => {
confirmCount += 1;
return true;
},
});
const first = await prompt.maybeOffer(PUBKEY);
assert.equal(first.offered, false);
assert.equal(first.reason, "no-remote");
assert.equal(confirmCount, 0);
// Later a note appears; the offer is still available.
storageSync.hasRemote = async () => true;
const later = await prompt.maybeOffer(PUBKEY);
assert.equal(later.offered, true);
assert.equal(later.accepted, true);
assert.deepEqual(later.restored, ["storage"]);
});