Files
bitvid/tests/services/trustBootstrap.test.mjs
T
thePR0M3TH3ANandClaude Opus 5 f48150ed6b [ci] Fix the stale tests that had CI red
CI has been failing on unstable for weeks, across four unit files and a set of
e2e specs. None of it was a product regression: every failure is a test that
encodes behavior a later commit deliberately changed. Each is corrected against
the spec that superseded it, not adjusted until it passed.

Unit:

- video-modal-moderation: the fixture set flat `blurThumbnail`/`trustedMuteCount`,
  but normalizeVideoModerationContext derives shouldShow from
  `original.blurThumbnail`/`trustedCount`, so the assertion ran against a
  context whose early return had just deleted the blur it was checking. 4525c1c6
  also made a lone trusted mute a non-blocking ranking signal (its sibling test
  was renamed to say exactly that; this one was missed). Fixture now mirrors
  what moderationDecorator emits.
- trustBootstrap: asserted a subscription to onWhitelistChange that 18e08bf2
  deliberately removed -- buildTrustedSeeds derives seeds from the super admin,
  editors and fallback seeds, so a whitelist change cannot alter them. The
  removal is now pinned so it is not reinstated by reflex.
- moderation-service: 18e08bf2 removed `status.whitelisted ||` from
  isTrustedReporter (being allowed to publish is not a statement that your
  reports carry weight). A whitelisted reporter now counts toward the report
  total but not toward trusted.
- playbackService_forcedSource: forced P2P now falls back to a hosted URL when
  the torrent definitively fails -- playbackService documents the change and its
  rollback. The old assertion did not merely fail, it HUNG: forced sources skip
  the start timeout, so the now-reachable hosted attempt waited forever on a
  <video> that never emits.

E2E:

- ui-modals: asserted exactly one `.bv-modal` per component page, but
  profile-modal.html also bundles #storageCorsModal (d7ad1516). Requires at
  least one and drives the primary, which is what the test's own injected
  querySelector already used.
- coverage-utility-modules: used `author:abc123`, which 9afbc0cb correctly began
  rejecting as undecodable. Uses a real pubkey for the success path and pins the
  rejection separately.
- webseed-playback: asserted the FINAL src equals the CDN URL while also
  tolerating "No playable source found" in its status check -- contradictory,
  since a failed attempt calls resetVideoElement() and clears src. Headless CI
  cannot decode the archive.org H.264 sample. Now records src attachments and
  asserts the URL was attached, which is the behavior under test.
- nostr-runtime-coverage: asserted a legacy plaintext session-actor entry reads
  back as null. Since 8692dac6 added per-account storage, readV1Entry PURGES the
  plaintext entry and the reader falls back to the sole remaining encrypted
  account -- so null no longer describes the security property. Asserts the
  property directly: no plaintext key survives, in the entry or in raw storage.

Also drops the syncAuthUiState share-gate wiring added in d141c316. It was
redundant: Application.updateShareNostrAuthState() already fires on init, login,
logout, pubkey-change and signer-change and forwards to
videoModal.setShareNostrAuthState(). That call short-circuited on its own
`if (!this.videoModal?.setShareNostrAuthState) return;` guard purely because the
method did not exist -- adding it (d141c316) was the whole fix. The tests now
pin that contract instead of a second, competing code path.

All 416 unit files, all three CI shards, both DM suites and lint are green.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-28 17:37:24 -04:00

202 lines
7.7 KiB
JavaScript

import { test, describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert/strict";
// Setup global mocks before imports
if (typeof global.localStorage === "undefined") {
global.localStorage = {
getItem: mock.fn(() => null),
setItem: mock.fn(),
removeItem: mock.fn(),
clear: mock.fn(),
};
}
// Dynamic imports to ensure globals are set before modules load
const { bootstrapTrustedSeeds } = await import("../../js/services/trustBootstrap.js");
const { setTrustSeedsEnabled } = await import("../../js/constants.js");
const { accessControl } = await import("../../js/accessControl.js");
const moderationService = (await import("../../js/services/moderationService.js")).default;
const nostrService = (await import("../../js/services/nostrService.js")).default;
const { userLogger, devLogger } = await import("../../js/utils/logger.js");
describe("trustBootstrap", () => {
let originalAccessControlMethods = {};
let originalModerationServiceMethods = {};
let originalNostrServiceMethods = {};
let originalUserLoggerMethods = {};
let originalDevLoggerMethods = {};
beforeEach(() => {
// Enable feature flag by default
setTrustSeedsEnabled(true);
// Mock logger to suppress noise
originalUserLoggerMethods = {
warn: userLogger.warn,
info: userLogger.info,
error: userLogger.error
};
originalDevLoggerMethods = {
warn: devLogger.warn,
info: devLogger.info,
error: devLogger.error
};
try {
userLogger.warn = mock.fn();
userLogger.info = mock.fn();
userLogger.error = mock.fn();
devLogger.warn = mock.fn();
devLogger.info = mock.fn();
devLogger.error = mock.fn();
} catch (e) {
// If frozen, we can't mock. That's fine.
}
// Mock accessControl
originalAccessControlMethods = {
waitForReady: accessControl.waitForReady,
getEditors: accessControl.getEditors,
onWhitelistChange: accessControl.onWhitelistChange,
onEditorsChange: accessControl.onEditorsChange,
onBlacklistChange: accessControl.onBlacklistChange,
};
accessControl.waitForReady = mock.fn(async () => {});
accessControl.getEditors = mock.fn(() => []);
accessControl.onWhitelistChange = mock.fn(() => {});
accessControl.onEditorsChange = mock.fn(() => {});
accessControl.onBlacklistChange = mock.fn(() => {});
// Mock moderationService
originalModerationServiceMethods = {
setTrustedSeeds: moderationService.setTrustedSeeds,
updateTrustedSeedOnlyStatus: moderationService.updateTrustedSeedOnlyStatus,
recomputeAllSummaries: moderationService.recomputeAllSummaries,
};
moderationService.setTrustedSeeds = mock.fn();
moderationService.updateTrustedSeedOnlyStatus = mock.fn();
moderationService.recomputeAllSummaries = mock.fn();
// Mock nostrService
originalNostrServiceMethods = {
nostrClient: nostrService.nostrClient,
};
nostrService.nostrClient = {
pool: {},
relays: ["wss://relay.example.com"], // Simulate ready state
};
});
afterEach(() => {
// Restore accessControl
accessControl.waitForReady = originalAccessControlMethods.waitForReady;
accessControl.getEditors = originalAccessControlMethods.getEditors;
accessControl.onWhitelistChange = originalAccessControlMethods.onWhitelistChange;
accessControl.onEditorsChange = originalAccessControlMethods.onEditorsChange;
accessControl.onBlacklistChange = originalAccessControlMethods.onBlacklistChange;
// Restore moderationService
moderationService.setTrustedSeeds = originalModerationServiceMethods.setTrustedSeeds;
moderationService.updateTrustedSeedOnlyStatus = originalModerationServiceMethods.updateTrustedSeedOnlyStatus;
moderationService.recomputeAllSummaries = originalModerationServiceMethods.recomputeAllSummaries;
// Restore nostrService
nostrService.nostrClient = originalNostrServiceMethods.nostrClient;
// Restore loggers if they were mocked
if (userLogger.warn.mock) userLogger.warn = originalUserLoggerMethods.warn;
if (userLogger.info.mock) userLogger.info = originalUserLoggerMethods.info;
if (userLogger.error.mock) userLogger.error = originalUserLoggerMethods.error;
if (devLogger.warn.mock) devLogger.warn = originalDevLoggerMethods.warn;
if (devLogger.info.mock) devLogger.info = originalDevLoggerMethods.info;
if (devLogger.error.mock) devLogger.error = originalDevLoggerMethods.error;
// Reset mocks
mock.reset();
});
it("should return early if feature is disabled", async () => {
setTrustSeedsEnabled(false);
await bootstrapTrustedSeeds();
assert.equal(moderationService.setTrustedSeeds.mock.callCount(), 0);
});
it("should apply seeds when accessControl is ready", async () => {
accessControl.getEditors.mock.mockImplementation(() => ["editor1"]);
await bootstrapTrustedSeeds();
assert.equal(moderationService.setTrustedSeeds.mock.callCount(), 1);
const seeds = moderationService.setTrustedSeeds.mock.calls[0].arguments[0];
assert.ok(seeds instanceof Set);
assert.ok(seeds.has("editor1"));
});
it("should subscribe to the accessControl lists that affect seeds", async () => {
await bootstrapTrustedSeeds();
assert.equal(accessControl.onEditorsChange.mock.callCount(), 1);
assert.equal(accessControl.onBlacklistChange.mock.callCount(), 1);
});
it("should NOT subscribe to whitelist changes", async () => {
// buildTrustedSeeds() derives seeds from the super admin, the editors list
// and the fallback seeds -- the whitelist is not an input, so re-applying
// seeds on a whitelist change is pure churn. 18e08bf2 ("refine trust
// controls") removed that subscription deliberately; this pins the removal
// so it is not reinstated by reflex.
await bootstrapTrustedSeeds();
assert.equal(accessControl.onWhitelistChange.mock.callCount(), 0);
});
it("should handle accessControl timeout and apply seeds anyway", async () => {
// Mock waitForAccessControl to timeout (reject)
accessControl.waitForReady.mock.mockImplementation(async () => {
throw new Error("accessControl ready check timed out");
});
await bootstrapTrustedSeeds();
// Even if it times out, it should eventually apply seeds (fallback path)
assert.equal(moderationService.setTrustedSeeds.mock.callCount(), 1);
});
it("should recompute summaries after applying seeds", async () => {
await bootstrapTrustedSeeds();
assert.equal(moderationService.recomputeAllSummaries.mock.callCount(), 1);
});
it("should wait for relays if hydration fails initially", async () => {
// First hydration attempt fails
accessControl.waitForReady.mock.mockImplementationOnce(async () => {
throw new Error("accessControl failed");
});
// Initially no relays
nostrService.nostrClient.relays = [];
// Start bootstrap in background since it might wait
const bootstrapPromise = bootstrapTrustedSeeds();
// Verify it hasn't applied seeds yet (it's waiting or retrying)
// Note: because we are using real timers (and maybe mock Date in other tests?),
// the code might run fast or slow.
// With no relays, runAsyncRetry -> waitForRelaysReady loops.
// So setTrustedSeeds should NOT be called immediately.
assert.equal(moderationService.setTrustedSeeds.mock.callCount(), 0);
// Simulate relays becoming ready after a short delay
setTimeout(() => {
nostrService.nostrClient.relays = ["wss://new.relay"];
}, 150); // > 125ms used in waitForRelaysReady
// Wait for logic to complete
await new Promise(resolve => setTimeout(resolve, 300));
// Should have applied seeds eventually
assert.ok(moderationService.setTrustedSeeds.mock.callCount() >= 1);
});
});