import {Client} from '@atproto/lex' import {PasswordSession} from '@atproto/lex-password-session' import {beforeEach, describe, expect, it, jest} from '@jest/globals' jest.mock('#/state/events', () => ({ emitNetworkConfirmed: jest.fn(), emitNetworkLost: jest.fn(), })) jest.mock('jwt-decode', () => ({ jwtDecode() { return {scope: 'com.atproto.access'} }, })) import {BLUESKY_PROXY_HEADER, CHAT_PROXY_SERVICE} from '#/lib/constants' import {app, chat, com} from '#/lexicons' import {configureGlobalAppLabelers} from '../additional-moderation-authorities' import { buildAppviewClient, buildChatClient, buildPdsClient, getUnauthenticatedThrowingClient, NotAuthenticatedError, routeSessionToPds, } from '../clients' import {sessionAccountToSessionData} from '../session-data' import { asFetch, DID, DIDDOC_PDS_HOST, HANDLE, json, makeAccount, makeDidDoc, makeMockFetch, type MockFetch, PDS_HOST, SERVICE, urlsOf, } from './mock-fetch' const PROFILE_BODY = { did: DID, handle: HANDLE, } /** A mock fetch that answers `getProfile` and records every request. */ function makeProfileFetch(): MockFetch { return makeMockFetch({ 'app.bsky.actor.getProfile': () => json(PROFILE_BODY), }) } /** A live `PasswordSession` whose whole network path is the mock fetch. */ function makeSession(fetchMock: MockFetch, didDocPdsUrl?: string) { const account = makeAccount() return new PasswordSession( { ...sessionAccountToSessionData(account), ...(didDocPdsUrl ? {didDoc: makeDidDoc(didDocPdsUrl)} : {}), }, {fetch: asFetch(fetchMock)}, ) } /** The `init` a mock fetch was called with for a given nsid. */ function initFor(mock: MockFetch, nsid: string): RequestInit | undefined { const call = mock.mock.calls.find(c => { const url = c[0] instanceof URL ? c[0].href : String(c[0]) return url.includes(`/xrpc/${nsid}`) }) return call?.[1] } /** The headers a mock fetch was called with for a given nsid. */ function headersFor(mock: MockFetch, nsid: string): Headers { return new Headers(initFor(mock, nsid)?.headers) } describe('buildAppviewClient', () => { let fetchMock: MockFetch beforeEach(() => { fetchMock = makeProfileFetch() configureGlobalAppLabelers([]) }) it('passes through the session did', () => { const client = buildAppviewClient(makeSession(fetchMock)) expect(client).toBeInstanceOf(Client) expect(client.did).toBe(DID) }) it('routes client.call through the session to the network', async () => { const client = buildAppviewClient(makeSession(fetchMock)) const body = await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) expect(body.handle).toBe(HANDLE) expect(urlsOf(fetchMock).join()).toContain(`actor=${HANDLE}`) }) it('emits the appview proxy header', async () => { const client = buildAppviewClient(makeSession(fetchMock)) await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) expect( headersFor(fetchMock, 'app.bsky.actor.getProfile').get('atproto-proxy'), ).toBe(BLUESKY_PROXY_HEADER.get()) }) it('emits an account subscription exactly once', async () => { const client = buildAppviewClient(makeSession(fetchMock)) client.setLabelers(['did:plc:labeler']) await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) const labelers = headersFor(fetchMock, 'app.bsky.actor.getProfile').get( 'atproto-accept-labelers', ) /* * The client is the only producer of this header now, so a duplicate would * mean lex itself emitted the same DID twice. */ const entries = labelers! .split(',') .filter(l => l.includes('did:plc:labeler')) expect(entries).toHaveLength(1) }) it('emits a global app labeler once, redacted', async () => { /* * The global static is the ONLY producer of the redacted authorities - no * agent stamps them any more - and lex suffixes them with `;redact`. An * account subscription that also listed the same DID would produce a second, * non-redacting entry, which is what `applyLabelersToClient` filters against. */ configureGlobalAppLabelers(['did:plc:global-labeler']) const client = buildAppviewClient(makeSession(fetchMock)) await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) const labelers = headersFor(fetchMock, 'app.bsky.actor.getProfile').get( 'atproto-accept-labelers', ) const entries = labelers! .split(',') .map(l => l.trim()) .filter(l => l.includes('did:plc:global-labeler')) expect(entries).toEqual(['did:plc:global-labeler;redact']) }) it('sends the session access token', async () => { const client = buildAppviewClient(makeSession(fetchMock)) await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) expect( headersFor(fetchMock, 'app.bsky.actor.getProfile').get('authorization'), ).toBe('Bearer access-jwt') }) }) describe('buildPdsClient', () => { let fetchMock: MockFetch beforeEach(() => { fetchMock = makeProfileFetch() configureGlobalAppLabelers([]) }) it('is a distinct client from the appview client over the same session', () => { const session = makeSession(fetchMock) expect(buildPdsClient(session)).not.toBe(buildAppviewClient(session)) }) it('sends the session access token', async () => { await buildPdsClient(makeSession(fetchMock)).call( com.atproto.server.getSession, {}, ) expect( headersFor(fetchMock, 'com.atproto.server.getSession').get( 'authorization', ), ).toBe('Bearer access-jwt') }) it('emits neither the proxy nor any labeler header', async () => { /* * The load-bearing difference from the appview client: a PDS request must * reach the account host itself rather than being proxied onward, and it is * not an appview read, so it carries no moderation authorities either. */ configureGlobalAppLabelers(['did:plc:global-labeler']) await buildPdsClient(makeSession(fetchMock)).call( com.atproto.server.getSession, {}, ) const headers = headersFor(fetchMock, 'com.atproto.server.getSession') expect(headers.get('atproto-proxy')).toBeNull() expect(headers.get('atproto-accept-labelers')).toBeNull() }) it('resolves the relative xrpc path against the account host', async () => { /* * lex hands its fetchHandler an origin-less `/xrpc/` path; the session * absolutizes it against its didDoc endpoint or, absent one, its service. */ await buildPdsClient(makeSession(fetchMock)).call( com.atproto.server.getSession, {}, ) expect(urlsOf(fetchMock)).toContain( `${SERVICE}/xrpc/com.atproto.server.getSession`, ) }) }) describe('buildChatClient', () => { let fetchMock: MockFetch beforeEach(() => { fetchMock = makeProfileFetch() configureGlobalAppLabelers([]) }) it('is a distinct client from the pds client over the same session', () => { const session = makeSession(fetchMock) expect(buildChatClient(session)).not.toBe(buildPdsClient(session)) }) it('emits the chat proxy header exactly once, with the session token', async () => { /* the stub body fails listConvos output validation; headers are recorded pre-parse */ await buildChatClient(makeSession(fetchMock)) .call(chat.bsky.convo.listConvos, {}) .catch(() => {}) const headers = headersFor(fetchMock, 'chat.bsky.convo.listConvos') /* * An exact match, not `toContain`: `Headers` comma-joins repeated entries * for the same name, so a second contributor would show up here. */ expect(headers.get('atproto-proxy')).toBe(CHAT_PROXY_SERVICE) expect(headers.get('authorization')).toBe('Bearer access-jwt') }) it('emits a global app labeler once, redacted', async () => { const client = buildChatClient(makeSession(fetchMock)) configureGlobalAppLabelers(['did:plc:global-labeler']) await client.call(chat.bsky.convo.listConvos, {}).catch(() => {}) const labelers = headersFor(fetchMock, 'chat.bsky.convo.listConvos').get( 'atproto-accept-labelers', ) const entries = labelers! .split(',') .map(l => l.trim()) .filter(l => l.includes('did:plc:global-labeler')) expect(entries).toEqual(['did:plc:global-labeler;redact']) }) it('emits an account subscription exactly once', async () => { const client = buildChatClient(makeSession(fetchMock)) client.setLabelers(['did:plc:labeler']) await client.call(chat.bsky.convo.listConvos, {}).catch(() => {}) const labelers = headersFor(fetchMock, 'chat.bsky.convo.listConvos').get( 'atproto-accept-labelers', ) const entries = labelers! .split(',') .filter(l => l.includes('did:plc:labeler')) expect(entries).toHaveLength(1) }) }) describe('routeSessionToPds', () => { let fetchMock: MockFetch beforeEach(() => { fetchMock = makeProfileFetch() }) it('sends a request to the pinned host rather than the login service', async () => { /* * The entryway case, and the reason this shim exists: an account whose * service is `bsky.social` but whose PDS is elsewhere, with no didDoc yet * (the synchronous resume fast path, i.e. the common cold start). Without * the shim the session would resolve against its service and every request * of that cold start would go to the entryway. */ const session = makeSession(fetchMock) const client = buildPdsClient(routeSessionToPds(session, PDS_HOST)) await client.call(com.atproto.server.getSession, {}) expect(urlsOf(fetchMock)).toEqual([ `${PDS_HOST}/xrpc/com.atproto.server.getSession`, ]) }) it('keeps the session auth lifecycle on the pinned host', async () => { const session = makeSession(fetchMock) const client = buildPdsClient(routeSessionToPds(session, PDS_HOST)) await client.call(com.atproto.server.getSession, {}) expect( headersFor(fetchMock, 'com.atproto.server.getSession').get( 'authorization', ), ).toBe('Bearer access-jwt') }) it('passes through the session did', () => { const session = makeSession(fetchMock) expect(routeSessionToPds(session, PDS_HOST).did).toBe(DID) }) it('pins the stored host even when the session carries a different didDoc endpoint', async () => { /* * The narrowing this shim accepts versus the session manager it replaces: * the manager preferred a didDoc endpoint once one arrived, whereas an * absolute URL handed to `session.fetchHandler` survives `new URL(path, * base)` untouched, so the stored host wins for the bundle's lifetime. That * only matters if the account's PDS moved, and the next cold start pins the * newly persisted endpoint. */ const session = makeSession(fetchMock, DIDDOC_PDS_HOST) const client = buildPdsClient(routeSessionToPds(session, PDS_HOST)) await client.call(com.atproto.server.getSession, {}) expect(urlsOf(fetchMock)).toEqual([ `${PDS_HOST}/xrpc/com.atproto.server.getSession`, ]) }) it('lets the session route by didDoc when nothing is pinned', async () => { /* * The counterpart: a bundle built with no stored `pdsUrl` goes straight over * the session, which resolves against its own didDoc endpoint. */ const client = buildPdsClient(makeSession(fetchMock, DIDDOC_PDS_HOST)) await client.call(com.atproto.server.getSession, {}) expect(urlsOf(fetchMock)).toEqual([ `${DIDDOC_PDS_HOST}/xrpc/com.atproto.server.getSession`, ]) }) }) describe('getUnauthenticatedThrowingClient', () => { it('is a stable singleton with no did', () => { const client = getUnauthenticatedThrowingClient() expect(client.did).toBeUndefined() /* identity is stable so it is safe in React Query keys */ expect(getUnauthenticatedThrowingClient()).toBe(client) }) it('rejects any call with NotAuthenticatedError as the cause, with no fetch', async () => { /* * The throwing fetchHandler fires before any network I/O. lex-client wraps a * fetchHandler throw in an internal error whose `cause` is the original, so * the NotAuthenticatedError surfaces there. */ const fetchMock = makeProfileFetch() const err = await getUnauthenticatedThrowingClient() .call(com.atproto.server.getSession, {}) .then(() => undefined) .catch((e: unknown) => e) expect((err as Error).cause).toBeInstanceOf(NotAuthenticatedError) expect(((err as Error).cause as Error).name).toBe('NotAuthenticatedError') expect(((err as Error).cause as Error).message).toBe( 'Not authenticated: this operation requires an active session', ) expect(fetchMock).not.toHaveBeenCalled() }) })