Files
bitvid/tests/services/profileMetadataService.test.mjs

259 lines
8.8 KiB
JavaScript

import { describe, it, mock, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
fetchProfileMetadataBatch,
fetchProfileMetadata,
ensureProfileMetadataSubscription
} from "../../js/services/profileMetadataService.js";
describe("profileMetadataService", () => {
let mockNostr;
let mockPool;
let mockLogger;
beforeEach(() => {
mockPool = {
list: mock.fn(async () => []),
sub: mock.fn(() => ({
on: mock.fn(),
unsub: mock.fn(),
})),
};
mockNostr = {
pool: mockPool,
relays: ["wss://relay.example.com"],
readRelays: ["wss://read.example.com"],
};
mockLogger = {
warn: mock.fn(),
log: mock.fn(),
debug: mock.fn(),
};
});
describe("fetchProfileMetadataBatch", () => {
it("should return empty map if no pubkeys provided", async () => {
const results = await fetchProfileMetadataBatch({
pubkeys: [],
nostr: mockNostr,
logger: mockLogger,
});
assert.equal(results.size, 0);
assert.equal(mockPool.list.mock.callCount(), 0);
});
it("should fetch profiles for provided pubkeys", async () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
const mockEvent = {
pubkey,
created_at: 1000,
content: JSON.stringify({ name: "Alice", about: "Tester" }),
};
mockPool.list.mock.mockImplementation(async () => [mockEvent]);
const results = await fetchProfileMetadataBatch({
pubkeys: [pubkey],
nostr: mockNostr,
logger: mockLogger,
});
assert.equal(results.size, 1);
const profileData = results.get(pubkey);
assert.ok(profileData);
assert.equal(profileData.profile.name, "Alice");
assert.equal(profileData.profile.about, "Tester");
// Verify pool.list was called
assert.equal(mockPool.list.mock.callCount(), 1); // Once per relay (mockNostr has 2, but logic merges? Wait, let's check logic)
// Logic: resolveRelays prefers readRelays if available.
// resolveRelays(relays, nostr)
// if relays arg is passed, use it.
// else if nostr.readRelays, use it.
// else if nostr.relays, use it.
// Here we didn't pass relays, so it should use readRelays ("wss://read.example.com")
// So call count should be 1 (since 1 relay in readRelays)
assert.equal(mockPool.list.mock.callCount(), 1);
const callArgs = mockPool.list.mock.calls[0].arguments;
assert.deepEqual(callArgs[0], ["wss://read.example.com"]);
assert.deepEqual(callArgs[1], [{ kinds: [0], authors: [pubkey], limit: 1 }]);
});
it("should handle relay failures gracefully", async () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000002";
mockPool.list.mock.mockImplementation(async () => {
throw new Error("Relay error");
});
const results = await fetchProfileMetadataBatch({
pubkeys: [pubkey],
nostr: mockNostr,
logger: mockLogger,
});
assert.equal(results.size, 0);
assert.equal(mockLogger.warn.mock.callCount(), 1);
});
it("should parse profile content correctly", async () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000003";
const content = JSON.stringify({
display_name: "Bob",
picture: "https://example.com/bob.jpg",
website: "https://bob.com",
banner: "https://bob.com/banner.jpg",
lud16: "[email protected]",
lud06: "lnurl1...",
});
const mockEvent = {
pubkey,
created_at: 2000,
content,
};
mockPool.list.mock.mockImplementation(async () => [mockEvent]);
const results = await fetchProfileMetadataBatch({
pubkeys: [pubkey],
nostr: mockNostr,
logger: mockLogger,
});
const profile = results.get(pubkey).profile;
assert.equal(profile.name, "Bob");
assert.equal(profile.picture, "https://example.com/bob.jpg");
assert.equal(profile.website, "https://bob.com");
assert.equal(profile.banner, "https://bob.com/banner.jpg");
assert.equal(profile.lud16, "[email protected]");
assert.equal(profile.lud06, "lnurl1...");
});
it("should deduplicate concurrent requests (inflight)", async () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000004";
let resolveList;
const listPromise = new Promise(r => { resolveList = r; });
mockPool.list.mock.mockImplementation(() => listPromise);
// Start two requests
const p1 = fetchProfileMetadataBatch({ pubkeys: [pubkey], nostr: mockNostr, logger: mockLogger });
const p2 = fetchProfileMetadataBatch({ pubkeys: [pubkey], nostr: mockNostr, logger: mockLogger });
// Verify only one list call was made so far
// Wait a tick
await new Promise(r => setTimeout(r, 0));
assert.equal(mockPool.list.mock.callCount(), 1);
// Resolve
resolveList([{ pubkey, created_at: 1, content: "{}" }]);
await Promise.all([p1, p2]);
assert.equal(mockPool.list.mock.callCount(), 1);
});
});
describe("fetchProfileMetadata", () => {
it("should return null for invalid pubkey", async () => {
const result = await fetchProfileMetadata("invalid", { nostr: mockNostr });
assert.equal(result, null);
});
it("should return single profile result", async () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000005";
mockPool.list.mock.mockImplementation(async () => [{
pubkey,
created_at: 1,
content: JSON.stringify({ name: "Single" })
}]);
const result = await fetchProfileMetadata(pubkey, { nostr: mockNostr, logger: mockLogger });
assert.equal(result.profile.name, "Single");
});
});
describe("ensureProfileMetadataSubscription", () => {
it("should return null if nostr pool is missing", () => {
const res = ensureProfileMetadataSubscription({
pubkey: "abc",
nostr: {}, // No pool
});
assert.equal(res, null);
});
it("should create a subscription via relaySubscriptionService", () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000006";
const mockSub = {
on: mock.fn(),
unsub: mock.fn(),
};
mockPool.sub.mock.mockImplementation(() => mockSub);
const sub = ensureProfileMetadataSubscription({
pubkey,
nostr: mockNostr,
logger: mockLogger,
});
// relaySubscriptionService returns the subscription object
assert.equal(sub, mockSub);
assert.equal(mockPool.sub.mock.callCount(), 1);
const args = mockPool.sub.mock.calls[0].arguments;
// check relays
assert.deepEqual(args[0], ["wss://read.example.com"]);
// check filters
assert.deepEqual(args[1], [{ kinds: [0], authors: [pubkey], limit: 1 }]);
});
it("should handle onProfile callback", () => {
const pubkey = "0000000000000000000000000000000000000000000000000000000000000007";
let capturedOnEvent;
// We need to capture the onEvent passed to relaySubscriptionService.ensureSubscription
// Since we can't easily mock the internal import of relaySubscriptionService,
// we rely on the fact that relaySubscriptionService attaches a listener to the sub.
// BUT, relaySubscriptionService attaches `subscription.on('event', ...)`
// So if we mock `sub.on`, we can capture the handler provided by relaySubscriptionService.
let relayServiceEventHandler;
const mockSub = {
on: mock.fn((event, handler) => {
if (event === 'event') {
relayServiceEventHandler = handler;
}
}),
unsub: mock.fn(),
};
mockPool.sub.mock.mockImplementation(() => mockSub);
const onProfileSpy = mock.fn();
ensureProfileMetadataSubscription({
pubkey,
nostr: mockNostr,
logger: mockLogger,
onProfile: onProfileSpy,
});
// Now simulate an event coming from the pool subscription
const event = {
pubkey,
content: JSON.stringify({ name: "Live Update" }),
};
if (relayServiceEventHandler) {
relayServiceEventHandler(event);
}
assert.equal(onProfileSpy.mock.callCount(), 1);
const calledArg = onProfileSpy.mock.calls[0].arguments[0];
assert.equal(calledArg.profile.name, "Live Update");
assert.equal(calledArg.event, event);
});
});
});