mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
215 lines
7.3 KiB
JavaScript
215 lines
7.3 KiB
JavaScript
// Scenario (SCN-login-defers-dms):
|
|
// Given a user logs in via the auth session coordinator,
|
|
// When the post-login hydration runs (profile + feed-driving lists settle),
|
|
// Then the coordinator MUST NOT eagerly load the full direct-message history
|
|
// (a cold limit:50 DM load is a burst of nip-07 decrypts that competes with
|
|
// blocks/subscriptions/hashtags for the single-threaded extension channel
|
|
// during the fragile handshake window — see KNOWN_BUGS #0). DMs load lazily
|
|
// when the Messages tab is opened instead.
|
|
// And the DM loading state is reported as "deferred" (not "ready"/"loading"),
|
|
// And the lightweight unread indicator still runs so the badge stays accurate.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// Minimal browser shim: handleAuthLogin reads window.location.search. The unit
|
|
// suite shares one process, so a sibling test may have already installed a
|
|
// window (possibly without a usable location, or carrying a "?v=" param that
|
|
// routes login differently) — normalize it for this scenario either way.
|
|
if (typeof globalThis.window === "undefined") {
|
|
globalThis.window = {};
|
|
}
|
|
globalThis.window.location = { search: "", href: "http://localhost/" };
|
|
|
|
const { createAuthSessionCoordinator } = await import(
|
|
"../../js/app/authSessionCoordinator.js"
|
|
);
|
|
|
|
const ACTIVE_PUBKEY = "a".repeat(64);
|
|
|
|
function makeCoordinator({ loadDirectMessages, refreshUnreadDmIndicator }) {
|
|
const noop = () => {};
|
|
const asyncNoop = async () => {};
|
|
const logger = { warn: noop, info: noop, log: noop, error: noop, debug: noop };
|
|
|
|
const coord = createAuthSessionCoordinator({
|
|
devLogger: logger,
|
|
userLogger: logger,
|
|
nostrClient: {
|
|
// Signer resolves so the readiness gate completes (non-extension type so
|
|
// it doesn't attempt a real permission prompt).
|
|
ensureActiveSignerForPubkey: async () => ({ type: "nsec", pubkey: ACTIVE_PUBKEY }),
|
|
ensureExtensionPermissions: async () => ({ ok: true }),
|
|
},
|
|
accessControl: { ensureReady: asyncNoop, getBlacklist: () => [] },
|
|
userBlocks: {
|
|
getBlockedPubkeys: () => [],
|
|
seedWithNpubs: asyncNoop,
|
|
},
|
|
subscriptions: { ensureLoaded: asyncNoop, lastLoadError: null },
|
|
hashtagPreferences: { load: asyncNoop, lastLoadError: null },
|
|
storageService: {},
|
|
relayManager: {},
|
|
torrentClient: {},
|
|
getHashViewName: () => "",
|
|
setHashView: noop,
|
|
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
|
RELAY_UI_BATCH_DELAY_MS: 0,
|
|
sanitizeRelayList: (x) => x,
|
|
buildDmRelayListEvent: noop,
|
|
publishEventToRelays: noop,
|
|
assertAnyRelayAccepted: noop,
|
|
queueSignEvent: noop,
|
|
bootstrapTrustedSeeds: noop,
|
|
getModerationSettings: () => ({}),
|
|
getActiveProfilePubkey: () => null,
|
|
});
|
|
|
|
// Collaborators the login flow touches on `this` — stub as inert so the flow
|
|
// runs end to end without real UI/services.
|
|
coord.pubkey = ACTIVE_PUBKEY;
|
|
coord.profileController = null;
|
|
coord.commentController = null;
|
|
coord.nwcSettingsService = null;
|
|
coord.authService = {
|
|
loadBlocksForPubkey: async () => true,
|
|
};
|
|
coord.nostrService = {
|
|
loadDirectMessages,
|
|
listDirectMessageConversationSummaries: async () => [],
|
|
};
|
|
|
|
const inert = [
|
|
"applyAuthenticatedUiState",
|
|
"capturePermissionPromptFromError",
|
|
"dispatchAuthChange",
|
|
"forceRefreshAllProfiles",
|
|
"maybeShowExperimentalLoginWarning",
|
|
"reinitializeVideoListView",
|
|
"renderSavedProfiles",
|
|
"refreshAllVideoGrids",
|
|
"resetPermissionPromptState",
|
|
"resetViewLoggingState",
|
|
"updateActiveProfileUI",
|
|
"updateCachedHashtagPreferences",
|
|
"updateShareNostrAuthState",
|
|
];
|
|
for (const name of inert) {
|
|
coord[name] = noop;
|
|
}
|
|
coord.normalizeHexPubkey = (x) => x;
|
|
coord.refreshUnreadDmIndicator = refreshUnreadDmIndicator;
|
|
coord.updateAuthLoadingState = (patch) => {
|
|
coord.authLoadingState = { ...(coord.authLoadingState || {}), ...patch };
|
|
};
|
|
coord.authLoadingState = { profile: "idle", lists: "idle", dms: "idle" };
|
|
|
|
return coord;
|
|
}
|
|
|
|
test("login does not eagerly load DM history; it is deferred", async () => {
|
|
// Re-normalize at call time in case a sibling test mutated window/location.
|
|
if (typeof globalThis.window === "undefined") {
|
|
globalThis.window = {};
|
|
}
|
|
globalThis.window.location = { search: "", href: "http://localhost/" };
|
|
|
|
let loadDmCalls = 0;
|
|
let unreadIndicatorCalls = 0;
|
|
const coord = makeCoordinator({
|
|
loadDirectMessages: async () => {
|
|
loadDmCalls += 1;
|
|
return { ok: true };
|
|
},
|
|
refreshUnreadDmIndicator: async () => {
|
|
unreadIndicatorCalls += 1;
|
|
},
|
|
});
|
|
|
|
await coord.handleAuthLogin({
|
|
pubkey: ACTIVE_PUBKEY,
|
|
identityChanged: true,
|
|
postLogin: { profile: { name: "tester" } },
|
|
postLoginPromise: Promise.resolve({ profile: { name: "tester" } }),
|
|
});
|
|
|
|
// Let every post-login promise chain (profile -> dm/nwc, feed sync) settle.
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
|
|
assert.equal(
|
|
loadDmCalls,
|
|
0,
|
|
"login must NOT eagerly load DM history (it competes with feed lists for the nip-07 channel)",
|
|
);
|
|
assert.equal(
|
|
coord.authLoadingState.dms,
|
|
"deferred",
|
|
"DM loading state should be reported as deferred at login",
|
|
);
|
|
assert.ok(
|
|
unreadIndicatorCalls >= 1,
|
|
"the lightweight unread DM indicator should still run so the badge stays accurate",
|
|
);
|
|
});
|
|
|
|
test("login reloads the open video comment thread", async () => {
|
|
globalThis.window.location = { search: "", href: "http://localhost/" };
|
|
|
|
const coord = makeCoordinator({
|
|
loadDirectMessages: async () => ({ ok: true }),
|
|
refreshUnreadDmIndicator: async () => {},
|
|
});
|
|
const activeVideo = { id: "b".repeat(64), title: "Open video" };
|
|
const loadedVideos = [];
|
|
coord.videoModal = { getCurrentVideo: () => activeVideo };
|
|
coord.commentController = {
|
|
load: (video) => loadedVideos.push(video),
|
|
refreshAuthState: () => {
|
|
throw new Error("an open thread should be reloaded, not only refreshed");
|
|
},
|
|
};
|
|
|
|
await coord.handleAuthLogin({
|
|
pubkey: ACTIVE_PUBKEY,
|
|
identityChanged: true,
|
|
postLogin: { profile: { name: "tester" } },
|
|
postLoginPromise: Promise.resolve({ profile: { name: "tester" } }),
|
|
});
|
|
|
|
assert.deepEqual(loadedVideos, [activeVideo]);
|
|
});
|
|
|
|
test("duplicate login for the active identity joins the existing list sync", async () => {
|
|
const coord = makeCoordinator({
|
|
loadDirectMessages: async () => ({ ok: true }),
|
|
refreshUnreadDmIndicator: async () => {},
|
|
});
|
|
let releaseBlocks;
|
|
const blocksGate = new Promise((resolve) => {
|
|
releaseBlocks = resolve;
|
|
});
|
|
let blockLoads = 0;
|
|
coord.authService.loadBlocksForPubkey = async () => {
|
|
blockLoads += 1;
|
|
await blocksGate;
|
|
return true;
|
|
};
|
|
const detail = {
|
|
pubkey: ACTIVE_PUBKEY,
|
|
identityChanged: false,
|
|
postLogin: { profile: { name: "tester" } },
|
|
postLoginPromise: Promise.resolve({ profile: { name: "tester" } }),
|
|
};
|
|
|
|
await coord.handleAuthLogin(detail);
|
|
const duplicate = coord.handleAuthLogin({ ...detail });
|
|
for (let attempt = 0; attempt < 20 && blockLoads === 0; attempt += 1) {
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
}
|
|
assert.equal(blockLoads, 1, "duplicate login must not start a second list sync");
|
|
|
|
releaseBlocks();
|
|
await duplicate;
|
|
assert.equal(coord._authLoginSync, null, "dedupe guard releases after list sync settles");
|
|
});
|