Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/improsocial/impro
An extensible Bluesky client for web impro.social
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428import { describe, it } from "node:test";import assert from "node:assert/strict";import { PluginDataProvider } from "/js/plugins/pluginDataProvider.js";
function makeStubBridge() { const handlers = new Map(); return { addHostMethod: (name, handler) => handlers.set(name, handler), handlers, };}
function makeStubComputedMap(lookup) { const calls = []; const map = { get: (key) => { calls.push(key); return lookup(key); }, }; return { map, calls };}
function makeDataProvider({ dataLayer = {}, pluginRequests = null, slingshot = null, constellation = null, session = { did: "did:plc:me", handle: "me.test" },} = {}) { const provider = new PluginDataProvider({ dataLayer, pluginRequests, slingshot, constellation, session, }); const bridge = makeStubBridge(); provider.registerHostMethods(bridge); return { provider, handlers: bridge.handlers };}
describe("app.data host methods", () => { it("getProfile returns the hydrated profile from derived", async () => { const profiles = makeStubComputedMap((did) => ({ did, handle: "alice.test", })); const { handlers } = makeDataProvider({ dataLayer: { derived: { $hydratedProfiles: profiles.map } }, }); const result = await handlers.get("getProfile")(null, { did: "did:plc:abc", }); assert.deepEqual(profiles.calls, ["did:plc:abc"]); assert.deepEqual(result, { did: "did:plc:abc", handle: "alice.test" }); });
it("getProfile fetches on a cache miss and returns the basic hydrated profile", async () => { let loaded = false; const profiles = makeStubComputedMap((did) => loaded ? { did, handle: "alice.test" } : null, ); const ensureCalls = []; const { handlers } = makeDataProvider({ dataLayer: { derived: { $hydratedProfiles: profiles.map }, declarative: { ensureDetailedProfile: async (did) => { ensureCalls.push(did); loaded = true; }, }, }, }); const result = await handlers.get("getProfile")(null, { did: "did:plc:abc", }); assert.deepEqual(ensureCalls, ["did:plc:abc"]); assert.deepEqual(result, { did: "did:plc:abc", handle: "alice.test" }); });
it("getProfile returns null when the profile cannot be loaded", async () => { const { handlers } = makeDataProvider({ dataLayer: { derived: { $hydratedProfiles: makeStubComputedMap(() => null).map }, declarative: { ensureDetailedProfile: async () => { throw new Error("Profile not found"); }, }, }, }); const result = await handlers.get("getProfile")(null, { did: "did:plc:missing", }); assert.deepEqual(result, null); });
it("getPost fetches the post on a cache miss", async () => { const ensureCalls = []; const { handlers } = makeDataProvider({ dataLayer: { declarative: { ensurePost: async (uri) => { ensureCalls.push(uri); return { uri, record: { text: "fetched" } }; }, }, }, }); const result = await handlers.get("getPost")(null, { uri: "at://example/post/1", }); assert.deepEqual(ensureCalls, ["at://example/post/1"]); assert.deepEqual(result, { uri: "at://example/post/1", record: { text: "fetched" }, }); });
it("getPost returns null when the post cannot be loaded", async () => { const { handlers } = makeDataProvider({ dataLayer: { declarative: { ensurePost: async () => { throw new Error("Post not found"); }, }, }, }); const result = await handlers.get("getPost")(null, { uri: "at://example/post/gone", }); assert.deepEqual(result, null); });
it("getKnownFollowers resolves via the declarative layer", async () => { const knownFollowers = { followers: [{ did: "did:plc:follower" }] }; const ensureCalls = []; const { handlers } = makeDataProvider({ dataLayer: { declarative: { ensureKnownFollowers: async (did) => { ensureCalls.push(did); return knownFollowers; }, }, }, }); const result = await handlers.get("getKnownFollowers")(null, { did: "did:plc:abc", }); assert.deepEqual(ensureCalls, ["did:plc:abc"]); assert.deepEqual(result, knownFollowers); });
it("getKnownFollowers returns null when the list cannot be loaded", async () => { const { handlers } = makeDataProvider({ dataLayer: { declarative: { ensureKnownFollowers: async () => { throw new Error("Known followers not found"); }, }, }, }); const result = await handlers.get("getKnownFollowers")(null, { did: "did:plc:missing", }); assert.deepEqual(result, null); });
// The remaining ensure-backed read methods share one shape: resolve via // the declarative layer, null on failure. const ensureBackedMethods = [ { method: "getPostThread", ensure: "ensurePostThread", arg: { uri: "at://example/post/1" }, expectedKey: "at://example/post/1", }, { method: "getDetailedProfile", ensure: "ensureDetailedProfile", arg: { did: "did:plc:abc" }, expectedKey: "did:plc:abc", }, { method: "getList", ensure: "ensureList", arg: { uri: "at://example/list/1" }, expectedKey: "at://example/list/1", }, { method: "getFeedGenerator", ensure: "ensureFeedGenerator", arg: { uri: "at://example/feed/1" }, expectedKey: "at://example/feed/1", }, ];
for (const methodCase of ensureBackedMethods) { it(`${methodCase.method} resolves via the declarative layer`, async () => { const value = { data: methodCase.method }; const ensureCalls = []; const { handlers } = makeDataProvider({ dataLayer: { declarative: { [methodCase.ensure]: async (key) => { ensureCalls.push(key); return value; }, }, }, }); const result = await handlers.get(methodCase.method)( null, methodCase.arg, ); assert.deepEqual(ensureCalls, [methodCase.expectedKey]); assert.deepEqual(result, value); });
it(`${methodCase.method} returns null when the data cannot be loaded`, async () => { const { handlers } = makeDataProvider({ dataLayer: { declarative: { [methodCase.ensure]: async () => { throw new Error("not found"); }, }, }, }); const result = await handlers.get(methodCase.method)( null, methodCase.arg, ); assert.deepEqual(result, null); }); }
it("getCurrentUser returns the session identity, or null signed out", async () => { const { handlers } = makeDataProvider(); assert.deepEqual(await handlers.get("getCurrentUser")(null, {}), { did: "did:plc:me", handle: "me.test", });
const signedOut = makeDataProvider({ session: null }); assert.deepEqual( await signedOut.handlers.get("getCurrentUser")(null, {}), null, ); });
it("getCurrentUserProfile returns the full current user when signed in", async () => { const currentUser = { did: "did:plc:me", handle: "me.test", displayName: "Me", }; const { handlers } = makeDataProvider({ dataLayer: { declarative: { ensureCurrentUser: async () => currentUser }, }, }); assert.deepEqual( await handlers.get("getCurrentUserProfile")(null, {}), currentUser, ); });
it("getCurrentUserProfile returns null when signed out", async () => { let ensureCalled = false; const { handlers } = makeDataProvider({ session: null, dataLayer: { declarative: { ensureCurrentUser: async () => { ensureCalled = true; }, }, }, }); assert.deepEqual( await handlers.get("getCurrentUserProfile")(null, {}), null, ); assert.deepEqual(ensureCalled, false); });
it("getBacklinks validates the limit before querying", async () => { const linkCalls = []; const { handlers } = makeDataProvider({ constellation: { getLinks: async (query) => { linkCalls.push(query); return []; }, }, }); const handler = handlers.get("getBacklinks"); await assert.rejects( async () => handler(null, { subject: "did:plc:abc", source: "a:b", limit: 0 }), /invalid limit/, ); await assert.rejects( async () => handler(null, { subject: "did:plc:abc", source: "a:b", limit: 1001 }), /invalid limit/, ); assert.deepEqual(linkCalls, []); await handler(null, { subject: "did:plc:abc", source: "a:b", limit: 10 }); assert.deepEqual(linkCalls, [ { subject: "did:plc:abc", source: "a:b", limit: 10 }, ]); });});
describe("xrpcQuery host method", () => { const queryPlugin = { pluginId: "test-plugin" };
function makeXrpcProvider() { const xrpcCalls = []; const { handlers } = makeDataProvider({ pluginRequests: { pluginXrpcRequest: async (plugin, nsid, params) => { xrpcCalls.push({ plugin, nsid, params }); return { ok: true, status: 200, data: {} }; }, }, }); return { handler: handlers.get("xrpcQuery"), xrpcCalls }; }
it("delegates to pluginXrpcRequest with the calling plugin", async () => { const { handler, xrpcCalls } = makeXrpcProvider(); const result = await handler(queryPlugin, { nsid: "app.bsky.feed.getQuotes", params: { uri: "at://example/post/1", limit: 25 }, }); assert.deepEqual(result, { ok: true, status: 200, data: {} }); assert.deepEqual(xrpcCalls, [ { plugin: queryPlugin, nsid: "app.bsky.feed.getQuotes", params: { uri: "at://example/post/1", limit: 25 }, }, ]); });
it("requires an nsid", async () => { const { handler, xrpcCalls } = makeXrpcProvider(); await assert.rejects( async () => handler(queryPlugin, { params: {} }), /requires a nsid/, ); assert.deepEqual(xrpcCalls, []); });});
describe("host method argument guards", () => { // Guards run before the catch-to-null wrapping, so a missing argument is // a loud rejection rather than a silent null. const guardCases = [ { method: "getPost", payload: {}, message: /getPost requires a uri/ }, { method: "getProfile", payload: {}, message: /getProfile requires a did/ }, { method: "getDetailedProfile", payload: {}, message: /getDetailedProfile requires a did/, }, { method: "getKnownFollowers", payload: {}, message: /getKnownFollowers requires a did/, }, { method: "getPostThread", payload: {}, message: /getPostThread requires a uri/, }, { method: "getList", payload: {}, message: /getList requires a uri/ }, { method: "getFeedGenerator", payload: {}, message: /getFeedGenerator requires a uri/, }, { method: "getRecord", payload: { collection: "app.bsky.feed.post", rkey: "1" }, message: /getRecord requires a repo/, }, { method: "getRecord", payload: { repo: "did:plc:abc", rkey: "1" }, message: /getRecord requires a collection/, }, { method: "getRecord", payload: { repo: "did:plc:abc", collection: "app.bsky.feed.post" }, message: /getRecord requires a rkey/, }, { method: "getBacklinks", payload: { source: "a:b", limit: 10 }, message: /getBacklinks requires a subject/, }, { method: "getBacklinks", payload: { subject: "did:plc:abc", limit: 10 }, message: /getBacklinks requires a source/, }, ];
for (const guardCase of guardCases) { it(`${guardCase.method} rejects ${guardCase.message.source}`, async () => { const { handlers } = makeDataProvider(); await assert.rejects( async () => handlers.get(guardCase.method)(null, guardCase.payload), guardCase.message, ); }); }});