Merge pull request #1999 from PR0M3TH3AN/codex/add-test-stubs-for-permission-scenarios

Add tests for permission deferral during NIP-07 login and list loads
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-30 16:48:15 -05:00
commit 5f42eccc32
4 files changed
+324

No files matched your search

+84
View File
@@ -221,6 +221,90 @@ test(
},
);
test(
"load defers permission-required decrypts until explicitly enabled",
{ concurrency: false },
async () => {
const pubkey = "e".repeat(64);
const relayUrls = ["wss://relay-permissions.example"];
const originalRelayEntries = relayManager.getEntries();
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
const originalFetchIncremental = nostrClient.fetchListIncrementally;
const originalRelays = Array.isArray(nostrClient.relays)
? [...nostrClient.relays]
: nostrClient.relays;
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
? [...nostrClient.writeRelays]
: nostrClient.writeRelays;
const originalWindowNostr = window.nostr;
const event = {
id: "pref-permission",
created_at: 400,
pubkey,
content: "cipher-permission",
tags: [["encrypted", "nip04"]],
};
let fetchCalls = 0;
nostrClient.fetchListIncrementally = async () => {
fetchCalls += 1;
return fetchCalls === 1 ? [event] : [];
};
nostrClient.relays = relayUrls;
nostrClient.writeRelays = relayUrls;
const decryptCalls = [];
window.nostr = {
nip04: {
decrypt: async () => {
decryptCalls.push("nip04");
return JSON.stringify({
version: 1,
interests: ["Late"],
disinterests: [],
});
},
},
};
clearActiveSigner();
try {
await hashtagPreferences.load(pubkey, { allowPermissionPrompt: false });
assert.equal(
decryptCalls.length,
0,
"decryption should be skipped when permissions are deferred",
);
assert.equal(
hashtagPreferences.lastLoadError?.code,
"hashtag-preferences-permission-required",
"permission-required errors should be captured for deferred prompts",
);
assert.deepEqual(hashtagPreferences.getInterests(), []);
await hashtagPreferences.load(pubkey, { allowPermissionPrompt: true });
assert.equal(
decryptCalls.length,
1,
"explicit permission prompts should retry decryption",
);
assert.deepEqual(hashtagPreferences.getInterests(), ["late"]);
} finally {
relayManager.setEntries(originalRelayEntries, { allowEmpty: true, updateClient: false });
nostrClient.fetchListIncrementally = originalFetchIncremental;
nostrClient.relays = originalRelays;
nostrClient.writeRelays = originalWriteRelays;
window.nostr = originalWindowNostr;
}
},
);
test(
"interest and disinterest lists remain exclusive",
{ concurrency: false },
+36
View File
@@ -456,6 +456,42 @@ test("NIP-07 login quickly retries when a permission payload stalls", async () =
}
});
test("NIP-07 login does not wait for deferred permission grants", async () => {
clearStoredPermissions();
const env = setupLoginEnvironment();
const originalEnsurePermissions = nostrClient.ensureExtensionPermissions;
let completionPromise;
nostrClient.ensureExtensionPermissions = async () => {
completionPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("permission denied")), 300);
});
return { ok: true, completionPromise };
};
try {
const start = Date.now();
const result = await nip07Provider.login({ nostrClient });
const duration = Date.now() - start;
assert.equal(result.pubkey, HEX_PUBKEY);
assert.ok(
duration < 200,
`login should resolve before deferred permission grants (duration: ${duration}ms)`,
);
assert.ok(completionPromise, "completionPromise should be captured");
await assert.rejects(
completionPromise,
/permission denied/,
"deferred permission grants should still reject",
);
} finally {
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
env.restore();
nostrClient.logout();
clearStoredPermissions();
}
});
test("NIP-07 login surfaces enable permission errors", async () => {
clearStoredPermissions();
const env = setupLoginEnvironment({
+108
View File
@@ -541,6 +541,114 @@ test(
},
);
test(
"loadSubscriptions retries permission-required decrypts only when enabled",
async () => {
const SubscriptionsManager = subscriptions.constructor;
const manager = new SubscriptionsManager();
const originalRelays = Array.isArray(nostrClient.relays)
? [...nostrClient.relays]
: nostrClient.relays;
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
? [...nostrClient.writeRelays]
: nostrClient.writeRelays;
const originalPool = nostrClient.pool;
const originalFetchIncremental = nostrClient.fetchListIncrementally;
const originalEnsurePermissions = nostrClient.ensureExtensionPermissions;
const originalSigner = getActiveSigner();
const originalEnsureSubscription = manager.ensureSubscriptionListSubscription;
const hadWindow = typeof globalThis.window !== "undefined";
if (!hadWindow) {
globalThis.window = {};
}
const originalWindowNostr = globalThis.window.nostr;
const relayUrls = ["wss://relay-permission.example"];
nostrClient.relays = relayUrls;
nostrClient.writeRelays = relayUrls;
const originalRelayEntries = relayManager.getEntries();
relayManager.setEntries(relayUrls.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
const actor = "f".repeat(64);
const event = {
id: "event-permission",
created_at: 100,
pubkey: actor,
content: "cipher-permission",
tags: [["encrypted", "nip04"]],
};
nostrClient.pool = {};
nostrClient.fetchListIncrementally = async () => [event];
nostrClient.ensureExtensionPermissions = async () => ({ ok: true });
manager.ensureSubscriptionListSubscription = () => null;
clearActiveSigner();
const decryptCalls = [];
manager.decryptSubscriptionEvent = async (_event, _pubkey, options) => {
decryptCalls.push(options);
if (!options?.allowPermissionPrompt) {
const error = new Error("permissions required");
error.code = "subscriptions-permission-required";
return { ok: false, error };
}
return { ok: true, plaintext: JSON.stringify([["p", "pub-allowed"]]) };
};
try {
await manager.updateFromRelays(actor, { allowPermissionPrompt: false });
assert.equal(
decryptCalls.length,
1,
"should attempt decryption once without retry",
);
assert.equal(
manager.subscribedPubkeys.size,
0,
"subscriptions should remain empty when permissions are deferred",
);
await manager.updateFromRelays(actor, { allowPermissionPrompt: true });
assert.equal(
decryptCalls.length,
2,
"explicit permission prompts should trigger a retry",
);
assert.deepEqual(
Array.from(manager.subscribedPubkeys),
["pub-allowed"],
"retry with permission prompts should populate subscriptions",
);
} finally {
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
nostrClient.relays = originalRelays;
nostrClient.writeRelays = originalWriteRelays;
nostrClient.pool = originalPool;
nostrClient.fetchListIncrementally = originalFetchIncremental;
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
manager.ensureSubscriptionListSubscription = originalEnsureSubscription;
setActiveSigner(originalSigner);
if (typeof originalWindowNostr === "undefined") {
delete globalThis.window.nostr;
} else {
globalThis.window.nostr = originalWindowNostr;
}
if (!hadWindow) {
delete globalThis.window;
}
localStorage.clear();
profileCache.memoryCache.clear();
profileCache.activePubkey = null;
clearActiveSigner();
}
},
);
test(
"showSubscriptionVideos waits for nostrService warm-up and refreshes after updates",
async () => {
+96
View File
@@ -695,6 +695,102 @@ await (async () => {
}
})();
await (async () => {
const actor = "9".repeat(64);
const relays = ["wss://relay-login-mode.example"];
const originalRelayEntries = relayManager.getEntries();
relayManager.setEntries(relays.map(url => ({ url, mode: "both" })), { allowEmpty: false, updateClient: false });
const originalPool = nostrClient.pool;
const originalRelays = Array.isArray(nostrClient.relays)
? [...nostrClient.relays]
: nostrClient.relays;
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
? [...nostrClient.writeRelays]
: nostrClient.writeRelays;
const originalEnsurePermissions = nostrClient.ensureExtensionPermissions;
const originalSigner = getActiveSigner();
const originalNostr = window.nostr;
const originalBlocked = new Set(userBlocks.blockedPubkeys);
const originalBlockEventId = userBlocks.blockEventId;
const originalBlockEventCreatedAt = userBlocks.blockEventCreatedAt;
const originalMuteEventId = userBlocks.muteEventId;
const originalMuteEventCreatedAt = userBlocks.muteEventCreatedAt;
const originalLoaded = userBlocks.loaded;
let permissionRequests = 0;
nostrClient.ensureExtensionPermissions = async () => {
permissionRequests += 1;
return { ok: true };
};
nostrClient.relays = relays;
nostrClient.writeRelays = relays;
nostrClient.pool = {
list: async () => [
{
id: "event-login",
kind: 10000,
created_at: 100,
pubkey: actor,
content: "cipher-login",
tags: [["encrypted", "nip04"]],
},
],
};
const statusEvents = [];
const unsubscribeStatus = userBlocks.on(USER_BLOCK_EVENTS.STATUS, (detail) => {
statusEvents.push(detail);
});
clearActiveSigner();
window.nostr = undefined;
try {
const start = Date.now();
await userBlocks.loadBlocks(actor, { allowPermissionPrompt: false });
const duration = Date.now() - start;
assert.equal(
permissionRequests,
0,
"login-mode load should not request extension permissions",
);
assert.ok(
duration < 200,
`login-mode load should settle quickly (duration: ${duration}ms)`,
);
const permissionStatus = statusEvents.find(
(detail) => detail?.status === "permission-required",
);
assert.ok(permissionStatus, "login-mode load should emit permission-required status");
assert.equal(permissionStatus.allowPermissionPrompt, false);
} finally {
if (typeof unsubscribeStatus === "function") {
unsubscribeStatus();
}
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
userBlocks.blockedPubkeys = originalBlocked;
userBlocks.blockEventId = originalBlockEventId;
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
userBlocks.muteEventId = originalMuteEventId;
userBlocks.muteEventCreatedAt = originalMuteEventCreatedAt;
userBlocks.loaded = originalLoaded;
nostrClient.pool = originalPool;
nostrClient.relays = originalRelays;
nostrClient.writeRelays = originalWriteRelays;
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
window.nostr = originalNostr;
clearActiveSigner();
if (originalSigner) {
setActiveSigner(originalSigner);
}
}
})();
await (async () => {
const actor = "1".repeat(64);
const blocked = "2".repeat(64);