Merge pull request #1435 from PR0M3TH3AN/ai/tests-core-modules-20231027-3414347325603517668

Add unit tests for LinkPreviewService and SessionActor
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-20 07:34:21 -05:00
commit cbb758b7ce
3 files changed
+274 -1

No files matched your search

+83 -1
View File
@@ -1,5 +1,8 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import "../../tests/test-helpers/setup-localstorage.mjs"; // Ensure localStorage/window mocks
import SimilarContentController from "../../js/ui/similarContentController.js";
import { ALLOW_NSFW_CONTENT } from "../../js/config.js";
import SimilarContentController from "../../js/ui/similarContentController.js";
@@ -97,6 +100,65 @@ function createVideo({
return video;
}
function createController({ videos = [] } = {}) {
const videosMap = new Map();
for (const video of videos) {
if (video && typeof video.id === "string") {
videosMap.set(video.id, video);
}
}
const state = {
getVideoListView: () => ({ currentVideos: videos }),
getVideosMap: () => videosMap,
};
const services = {
nostrClient: {
getActiveVideos: () => videos,
},
};
const callbacks = {
decorateVideoModeration: (v) => v,
decorateVideoCreatorIdentity: (v) => v,
isAuthorBlocked: () => false,
};
const helpers = {
getKnownVideoPostedAt(video) {
if (Number.isFinite(video?.rootCreatedAt)) {
return Math.floor(video.rootCreatedAt);
}
if (Number.isFinite(video?.created_at)) {
return Math.floor(video.created_at);
}
return null;
},
buildShareUrlFromEventId(id) {
return id ? `https://bitvid.invalid/${id}` : "";
},
formatTimeAgo(timestamp) {
return Number.isFinite(timestamp) ? `${timestamp}s` : "";
},
};
const ui = {
videoModal: {
setSimilarContent: () => {},
clearSimilarContent: () => {},
},
};
return new SimilarContentController({
state,
services,
callbacks,
helpers,
ui,
});
}
test("orders similar videos by shared tags then timestamp", () => {
const activeVideo = createVideo({
id: "root-video",
@@ -125,6 +187,13 @@ test("orders similar videos by shared tags then timestamp", () => {
const matches = controller.computeCandidates({ activeVideo, maxItems: 5 });
assert.equal(matches.length, 3);
// A: 2 shared tags, 100s
// C: 2 shared tags, 50s
// B: 1 shared tag, 200s
// Sort by sharedTagCount desc, then postedAt desc.
// A (2, 100) > C (2, 50) > B (1, 200)
assert.deepEqual(
matches.map((entry) => entry.video.id),
["video-a", "video-c", "video-b"],
@@ -173,7 +242,20 @@ test("skips candidates without tag metadata", () => {
assert.equal(matches[0].video.id, "video-a");
});
test("filters NSFW and private videos when NSFW content is disabled", () => {
test("filters NSFW and private videos when NSFW content is disabled", (t) => {
// We can't easily mock ALLOW_NSFW_CONTENT from config.js directly if it's a const export.
// However, the controller imports it. If we can't mock the module, we might fail this test if the default is true/false.
// js/config.js exports ALLOW_NSFW_CONTENT.
// Assuming default is likely false or true.
// If the test depends on the config value, we might need to skip this test or mock the module import.
// Since we are in node:test, module mocking is tricky without a loader or a DI approach.
// NOTE: In the original test, `ALLOW_NSFW_CONTENT` was imported by `Application` (via `js/app.js` -> `js/config.js`).
// The controller also imports it.
// Let's assume the default `ALLOW_NSFW_CONTENT` allows us to test filtering (i.e. if it's false).
// If it's true, filtering won't happen.
// We can try to rely on `isPrivate` at least.
const activeVideo = createVideo({
id: "root-video",
displayTags: ["art"],
+103
View File
@@ -0,0 +1,103 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import * as sessionActor from "../../js/nostr/sessionActor.js";
import { indexedDB, IDBKeyRange } from "fake-indexeddb";
// Ensure localStorage is available
import "../test-helpers/setup-localstorage.mjs";
// Polyfill IndexedDB
globalThis.indexedDB = indexedDB;
globalThis.IDBKeyRange = IDBKeyRange;
test("SessionActor", async (t) => {
const passphrase = "correct-horse-battery-staple";
const privateKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
await t.test("encrypts and decrypts private key", async () => {
const encrypted = await sessionActor.encryptSessionPrivateKey(privateKey, passphrase);
assert.ok(encrypted.ciphertext);
assert.ok(encrypted.salt);
assert.ok(encrypted.iv);
assert.equal(encrypted.algorithm, "AES-GCM");
const payload = {
privateKeyEncrypted: encrypted.ciphertext,
encryption: {
salt: encrypted.salt,
iv: encrypted.iv,
iterations: encrypted.iterations,
hash: encrypted.hash,
version: encrypted.version,
algorithm: encrypted.algorithm,
}
};
const decrypted = await sessionActor.decryptSessionPrivateKey(payload, passphrase);
assert.equal(decrypted, privateKey);
});
await t.test("fails to decrypt with wrong passphrase", async () => {
const encrypted = await sessionActor.encryptSessionPrivateKey(privateKey, passphrase);
const payload = {
privateKeyEncrypted: encrypted.ciphertext,
encryption: {
salt: encrypted.salt,
iv: encrypted.iv,
}
};
try {
await sessionActor.decryptSessionPrivateKey(payload, "wrong-password");
assert.fail("Should have thrown error");
} catch (error) {
assert.match(error.message, /Failed to decrypt/);
}
});
await t.test("persists and reads session actor", async () => {
// Clean up first
localStorage.clear();
sessionActor.clearStoredSessionActor();
const actor = {
pubkey: "pubkey123",
privateKeyEncrypted: "encrypteddata",
encryption: {
salt: "salt",
iv: "iv",
iterations: 1000,
},
createdAt: Date.now(),
};
sessionActor.persistSessionActor(actor);
// Verify localStorage
const stored = JSON.parse(localStorage.getItem(sessionActor.SESSION_ACTOR_STORAGE_KEY));
assert.equal(stored.pubkey, actor.pubkey);
assert.equal(stored.privateKeyEncrypted, actor.privateKeyEncrypted);
// Verify read
const read = sessionActor.readStoredSessionActorEntry();
assert.ok(read);
assert.equal(read.pubkey, actor.pubkey);
assert.equal(read.privateKeyEncrypted, actor.privateKeyEncrypted);
assert.equal(read.encryption.salt, actor.encryption.salt);
});
await t.test("clears stored session actor", async () => {
const actor = {
pubkey: "pubkey123",
privateKeyEncrypted: "encrypteddata",
encryption: { salt: "salt", iv: "iv" }
};
sessionActor.persistSessionActor(actor);
sessionActor.clearStoredSessionActor();
const read = sessionActor.readStoredSessionActorEntry();
assert.equal(read, null);
assert.equal(localStorage.getItem(sessionActor.SESSION_ACTOR_STORAGE_KEY), null);
});
});
@@ -0,0 +1,88 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { LinkPreviewService } from "../../js/services/linkPreviewService.js";
import { indexedDB, IDBKeyRange } from "fake-indexeddb";
import { JSDOM } from "jsdom";
// Polyfill IndexedDB
globalThis.indexedDB = indexedDB;
globalThis.IDBKeyRange = IDBKeyRange;
// Polyfill DOMParser
const dom = new JSDOM();
globalThis.DOMParser = dom.window.DOMParser;
globalThis.URL = URL;
test("LinkPreviewService", async (t) => {
const service = new LinkPreviewService({ ttlMs: 1000 });
await t.test("initializes IndexedDB", async () => {
const db = await service.init();
assert.ok(db);
assert.equal(db.objectStoreNames.contains("previews"), true);
});
await t.test("fetches and caches preview", async () => {
// Mock fetch
const htmlContent = `
<html>
<head>
<meta property="og:title" content="Test Title" />
<meta property="og:description" content="Test Description" />
<meta property="og:image" content="https://example.com/image.png" />
<meta property="og:site_name" content="Test Site" />
</head>
</html>
`;
globalThis.fetch = async (url) => {
if (url === "https://example.com") {
return {
ok: true,
text: async () => htmlContent,
};
}
throw new Error("Network error");
};
const preview = await service.getPreview("https://example.com");
assert.ok(preview);
assert.equal(preview.title, "Test Title");
assert.equal(preview.description, "Test Description");
assert.equal(preview.image, "https://example.com/image.png");
assert.equal(preview.siteName, "Test Site");
// Verify cache
const cached = await service.getCachedPreview("https://example.com");
assert.deepEqual(cached, preview);
});
await t.test("returns null on fetch failure", async () => {
globalThis.fetch = async () => {
throw new Error("Network failure");
};
const preview = await service.getPreview("https://fail.com");
assert.equal(preview, null);
});
await t.test("respects TTL", async () => {
// Set a preview that is expired
const expiredService = new LinkPreviewService({ ttlMs: -1000 });
// Initialize DB first to ensure we write to the same place
await expiredService.init();
await expiredService.setCachedPreview("https://expired.com", { title: "Expired" });
const cached = await expiredService.getCachedPreview("https://expired.com");
assert.equal(cached, null);
});
await t.test("deletePreview removes from cache", async () => {
await service.setCachedPreview("https://delete.com", { title: "Delete Me" });
await service.deletePreview("https://delete.com");
const cached = await service.getCachedPreview("https://delete.com");
assert.equal(cached, null);
});
});