From 95d33f7b1187672ba97543aaaaa38726c1480328 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 27 Nov 2023 15:30:09 -0500 Subject: [PATCH] Support unauthed usage of feeds (#1884) * update local feedgens to not require a viewer where possible * update getFeed to use optional auth * test feeds w/ optional auth --- .../bsky/src/api/app/bsky/feed/getFeed.ts | 14 +- packages/bsky/src/auth.ts | 12 +- packages/bsky/src/context.ts | 4 + packages/bsky/src/feed-gen/best-of-follows.ts | 9 +- packages/bsky/src/feed-gen/bsky-team.ts | 2 +- packages/bsky/src/feed-gen/hot-classic.ts | 2 +- packages/bsky/src/feed-gen/mutuals.ts | 8 +- packages/bsky/src/feed-gen/types.ts | 2 +- packages/bsky/src/feed-gen/whats-hot.ts | 2 +- packages/bsky/src/feed-gen/with-friends.ts | 10 +- .../feed-generation.test.ts.snap | 328 +++++++++++++++++- packages/bsky/tests/feed-generation.test.ts | 61 +++- 12 files changed, 420 insertions(+), 34 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index 8af159decd3..bfae2caa2f5 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -33,7 +33,7 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getFeed({ - auth: ctx.authVerifierAnyAudience, + auth: ctx.authOptionalVerifierAnyAudience, handler: async ({ params, auth, req }) => { const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) @@ -98,13 +98,15 @@ const hydration = async (state: SkeletonState, ctx: Context) => { const noBlocksOrMutes = (state: HydrationState) => { const { viewer } = state.params - state.feedItems = state.feedItems.filter( - (item) => + state.feedItems = state.feedItems.filter((item) => { + if (!viewer) return true + return ( !state.bam.block([viewer, item.postAuthorDid]) && !state.bam.block([viewer, item.originatorDid]) && !state.bam.mute([viewer, item.postAuthorDid]) && - !state.bam.mute([viewer, item.originatorDid]), - ) + !state.bam.mute([viewer, item.originatorDid]) + ) + }) return state } @@ -130,7 +132,7 @@ type Context = { authorization?: string } -type Params = GetFeedParams & { viewer: string } +type Params = GetFeedParams & { viewer: string | null } type SkeletonState = { params: Params diff --git a/packages/bsky/src/auth.ts b/packages/bsky/src/auth.ts index b19e6860e5c..220be08fc32 100644 --- a/packages/bsky/src/auth.ts +++ b/packages/bsky/src/auth.ts @@ -28,14 +28,18 @@ export const authVerifier = return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } } } -export const authOptionalVerifier = - (idResolver: IdResolver, opts: { aud: string | null }) => - async (reqCtx: { req: express.Request; res: express.Response }) => { +export const authOptionalVerifier = ( + idResolver: IdResolver, + opts: { aud: string | null }, +) => { + const verify = authVerifier(idResolver, opts) + return async (reqCtx: { req: express.Request; res: express.Response }) => { if (!reqCtx.req.headers.authorization) { return { credentials: { did: null } } } - return authVerifier(idResolver, opts)(reqCtx) + return verify(reqCtx) } +} export const authOptionalAccessOrRoleVerifier = ( idResolver: IdResolver, diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index 21c01c38fbd..3488c6a5c02 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -94,6 +94,10 @@ export class AppContext { return auth.authVerifier(this.idResolver, { aud: null }) } + get authOptionalVerifierAnyAudience() { + return auth.authOptionalVerifier(this.idResolver, { aud: null }) + } + get authOptionalVerifier() { return auth.authOptionalVerifier(this.idResolver, { aud: this.cfg.serverDid, diff --git a/packages/bsky/src/feed-gen/best-of-follows.ts b/packages/bsky/src/feed-gen/best-of-follows.ts index c1d4ee4d21b..33c70ea81a4 100644 --- a/packages/bsky/src/feed-gen/best-of-follows.ts +++ b/packages/bsky/src/feed-gen/best-of-follows.ts @@ -1,4 +1,4 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' import { AlgoHandler, AlgoResponse } from './types' import { GenericKeyset, paginate } from '../db/pagination' @@ -7,12 +7,15 @@ import AppContext from '../context' const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - viewer: string, + viewer: string | null, ): Promise => { + if (!viewer) { + throw new AuthRequiredError('This feed requires being logged-in') + } + const { limit, cursor } = params const db = ctx.db.getReplica('feed') const feedService = ctx.services.feed(db) - const { ref } = db.db.dynamic // candidates are ranked within a materialized view by like count, depreciated over time. diff --git a/packages/bsky/src/feed-gen/bsky-team.ts b/packages/bsky/src/feed-gen/bsky-team.ts index 3592dd42e26..feb9539345e 100644 --- a/packages/bsky/src/feed-gen/bsky-team.ts +++ b/packages/bsky/src/feed-gen/bsky-team.ts @@ -14,7 +14,7 @@ const BSKY_TEAM: NotEmptyArray = [ const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - _viewer: string, + _viewer: string | null, ): Promise => { const { limit = 50, cursor } = params const db = ctx.db.getReplica('feed') diff --git a/packages/bsky/src/feed-gen/hot-classic.ts b/packages/bsky/src/feed-gen/hot-classic.ts index c042cea7116..d1595105f27 100644 --- a/packages/bsky/src/feed-gen/hot-classic.ts +++ b/packages/bsky/src/feed-gen/hot-classic.ts @@ -11,7 +11,7 @@ const NO_WHATS_HOT_LABELS: NotEmptyArray = ['!no-promote'] const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - _viewer: string, + _viewer: string | null, ): Promise => { const { limit = 50, cursor } = params const db = ctx.db.getReplica('feed') diff --git a/packages/bsky/src/feed-gen/mutuals.ts b/packages/bsky/src/feed-gen/mutuals.ts index 65a3311a524..86583ebaa56 100644 --- a/packages/bsky/src/feed-gen/mutuals.ts +++ b/packages/bsky/src/feed-gen/mutuals.ts @@ -3,16 +3,20 @@ import AppContext from '../context' import { paginate } from '../db/pagination' import { AlgoHandler, AlgoResponse } from './types' import { FeedKeyset, getFeedDateThreshold } from '../api/app/bsky/util/feed' +import { AuthRequiredError } from '@atproto/xrpc-server' const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - viewer: string, + viewer: string | null, ): Promise => { + if (!viewer) { + throw new AuthRequiredError('This feed requires being logged-in') + } + const { limit = 50, cursor } = params const db = ctx.db.getReplica('feed') const feedService = ctx.services.feed(db) - const { ref } = db.db.dynamic const mutualsSubquery = db.db diff --git a/packages/bsky/src/feed-gen/types.ts b/packages/bsky/src/feed-gen/types.ts index 11ebf53fb39..4693d64d4dd 100644 --- a/packages/bsky/src/feed-gen/types.ts +++ b/packages/bsky/src/feed-gen/types.ts @@ -11,7 +11,7 @@ export type AlgoResponse = { export type AlgoHandler = ( ctx: AppContext, params: SkeletonParams, - requester: string, + viewer: string | null, ) => Promise export type MountedAlgos = Record diff --git a/packages/bsky/src/feed-gen/whats-hot.ts b/packages/bsky/src/feed-gen/whats-hot.ts index 511c767804e..2376b98f185 100644 --- a/packages/bsky/src/feed-gen/whats-hot.ts +++ b/packages/bsky/src/feed-gen/whats-hot.ts @@ -21,7 +21,7 @@ const NO_WHATS_HOT_LABELS: NotEmptyArray = [ const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - _viewer: string, + _viewer: string | null, ): Promise => { const { limit, cursor } = params const db = ctx.db.getReplica('feed') diff --git a/packages/bsky/src/feed-gen/with-friends.ts b/packages/bsky/src/feed-gen/with-friends.ts index 0fd8f31c48e..1e6d345ffcc 100644 --- a/packages/bsky/src/feed-gen/with-friends.ts +++ b/packages/bsky/src/feed-gen/with-friends.ts @@ -3,16 +3,20 @@ import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/ge import { paginate } from '../db/pagination' import { AlgoHandler, AlgoResponse } from './types' import { FeedKeyset, getFeedDateThreshold } from '../api/app/bsky/util/feed' +import { AuthRequiredError } from '@atproto/xrpc-server' const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - requester: string, + viewer: string | null, ): Promise => { + if (!viewer) { + throw new AuthRequiredError('This feed requires being logged-in') + } + const { cursor, limit = 50 } = params const db = ctx.db.getReplica('feed') const feedService = ctx.services.feed(db) - const { ref } = db.db.dynamic const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid')) @@ -23,7 +27,7 @@ const handler: AlgoHandler = async ( .innerJoin('follow', 'follow.subjectDid', 'post.creator') .innerJoin('post_agg', 'post_agg.uri', 'post.uri') .where('post_agg.likeCount', '>=', 5) - .where('follow.creator', '=', requester) + .where('follow.creator', '=', viewer) .where('post.sortAt', '>', getFeedDateThreshold(sortFrom)) postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true }) diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index 1a5f8fc9281..ac9e0eee7a0 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -204,9 +204,9 @@ Array [ "muted": false, }, }, - "description": "Provides all feed candidates, blindly ignoring pagination limit", + "description": "Provides all feed candidates when authed", "did": "user(0)", - "displayName": "Bad Pagination", + "displayName": "Needs Auth", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, "uri": "record(4)", @@ -246,9 +246,9 @@ Array [ "muted": false, }, }, - "description": "Provides even-indexed feed candidates", + "description": "Provides all feed candidates, blindly ignoring pagination limit", "did": "user(0)", - "displayName": "Even", + "displayName": "Bad Pagination", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, "uri": "record(5)", @@ -288,14 +288,56 @@ Array [ "muted": false, }, }, + "description": "Provides even-indexed feed candidates", + "did": "user(0)", + "displayName": "Even", + "indexedAt": "1970-01-01T00:00:00.000Z", + "likeCount": 0, + "uri": "record(6)", + "viewer": Object {}, + }, + Object { + "cid": "cids(6)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, "description": "Provides all feed candidates", "did": "user(0)", "displayName": "All", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 2, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object { - "like": "record(7)", + "like": "record(8)", }, }, ] @@ -822,6 +864,280 @@ Array [ ] `; +exports[`feed generation getFeed resolves basic feed contents without auth. 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + }, + "cid": "cids(3)", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/sample-img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", + }, + Object { + "alt": "tests/sample-img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", + }, + ], + }, + "record": Object { + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + }, + "cid": "cids(6)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(3)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/sample-img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(4)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/sample-img/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(6)", + "uri": "record(3)", + }, + }, + }, + "text": "hi im carol", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + }, + "cid": "cids(7)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(8)", + "uri": "record(5)", + }, + "root": Object { + "cid": "cids(8)", + "uri": "record(5)", + }, + }, + "text": "of course", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(4)", + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + }, + "cid": "cids(8)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(5)", + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + }, + "cid": "cids(8)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(5)", + }, + }, + }, +] +`; + exports[`feed generation getFeed resolves basic feed contents. 1`] = ` Array [ Object { diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 09dfd92acc8..4970c13b31c 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -17,6 +17,9 @@ import { } from '../src/lexicon/types/app/bsky/feed/defs' import basicSeed from './seeds/basic' import { forSnapshot, paginateAll } from './_util' +import { AuthRequiredError } from '@atproto/xrpc-server' +import assert from 'assert' +import { XRPCError } from '@atproto/xrpc' describe('feed generation', () => { let network: TestNetwork @@ -33,6 +36,7 @@ describe('feed generation', () => { let feedUriBadPagination: string let feedUriPrime: string // Taken-down let feedUriPrimeRef: RecordRef + let feedUriNeedsAuth: string beforeAll(async () => { network = await TestNetwork.create({ @@ -52,11 +56,17 @@ describe('feed generation', () => { ) const evenUri = AtUri.make(alice, 'app.bsky.feed.generator', 'even') const primeUri = AtUri.make(alice, 'app.bsky.feed.generator', 'prime') + const needsAuthUri = AtUri.make( + alice, + 'app.bsky.feed.generator', + 'needs-auth', + ) gen = await network.createFeedGen({ [allUri.toString()]: feedGenHandler('all'), [evenUri.toString()]: feedGenHandler('even'), [feedUriBadPagination.toString()]: feedGenHandler('bad-pagination'), [primeUri.toString()]: feedGenHandler('prime'), + [needsAuthUri.toString()]: feedGenHandler('needs-auth'), }) const feedSuggestions = [ @@ -137,6 +147,16 @@ describe('feed generation', () => { }, sc.getHeaders(alice), ) + const needsAuth = await pdsAgent.api.app.bsky.feed.generator.create( + { repo: alice, rkey: 'needs-auth' }, + { + did: gen.did, + displayName: 'Needs Auth', + description: 'Provides all feed candidates when authed', + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) await network.processAll() await agent.api.com.atproto.admin.takeModerationAction( { @@ -161,6 +181,7 @@ describe('feed generation', () => { feedUriBadPagination = badPagination.uri feedUriPrime = prime.uri feedUriPrimeRef = new RecordRef(prime.uri, prime.cid) + feedUriNeedsAuth = needsAuth.uri }) it('feed gen records can be updated', async () => { @@ -198,11 +219,12 @@ describe('feed generation', () => { const paginatedAll: GeneratorView[] = results(await paginateAll(paginator)) - expect(paginatedAll.length).toEqual(4) + expect(paginatedAll.length).toEqual(5) expect(paginatedAll[0].uri).toEqual(feedUriOdd) - expect(paginatedAll[1].uri).toEqual(feedUriBadPagination) - expect(paginatedAll[2].uri).toEqual(feedUriEven) - expect(paginatedAll[3].uri).toEqual(feedUriAll) + expect(paginatedAll[1].uri).toEqual(feedUriNeedsAuth) + expect(paginatedAll[2].uri).toEqual(feedUriBadPagination) + expect(paginatedAll[3].uri).toEqual(feedUriEven) + expect(paginatedAll[4].uri).toEqual(feedUriAll) expect(paginatedAll.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down expect(forSnapshot(paginatedAll)).toMatchSnapshot() }) @@ -348,7 +370,9 @@ describe('feed generation', () => { {}, { headers: await network.serviceHeaders(sc.dids.bob) }, ) - expect(resEven.data.feeds.map((f) => f.likeCount)).toEqual([2, 0, 0, 0]) + expect(resEven.data.feeds.map((f) => f.likeCount)).toEqual([ + 2, 0, 0, 0, 0, + ]) expect(resEven.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down }) @@ -389,6 +413,16 @@ describe('feed generation', () => { expect(forSnapshot(feed.data.feed)).toMatchSnapshot() }) + it('resolves basic feed contents without auth.', async () => { + const feed = await agent.api.app.bsky.feed.getFeed({ feed: feedUriEven }) + expect(feed.data.feed.map((item) => item.post.uri)).toEqual([ + sc.posts[sc.dids.alice][0].ref.uriStr, + sc.posts[sc.dids.carol][0].ref.uriStr, + sc.replies[sc.dids.carol][0].ref.uriStr, + ]) + expect(forSnapshot(feed.data.feed)).toMatchSnapshot() + }) + it('paginates, handling replies and reposts.', async () => { const results = (results) => results.flatMap((res) => res.feed) const paginator = async (cursor?: string) => { @@ -461,6 +495,16 @@ describe('feed generation', () => { expect(feed.data['$auth']?.['iss']).toEqual(alice) }) + it('passes through auth error from feed.', async () => { + const tryGetFeed = agent.api.app.bsky.feed.getFeed({ + feed: feedUriNeedsAuth, + }) + const err = await tryGetFeed.catch((err) => err) + assert(err instanceof XRPCError) + expect(err.status).toBe(401) + expect(err.message).toBe('This feed requires auth') + }) + it('provides timing info in server-timing header.', async () => { const result = await agent.api.app.bsky.feed.getFeed( { feed: feedUriEven }, @@ -482,8 +526,13 @@ describe('feed generation', () => { }) const feedGenHandler = - (feedName: 'even' | 'all' | 'prime' | 'bad-pagination'): SkeletonHandler => + ( + feedName: 'even' | 'all' | 'prime' | 'bad-pagination' | 'needs-auth', + ): SkeletonHandler => async ({ req, params }) => { + if (feedName === 'needs-auth' && !req.headers.authorization) { + throw new AuthRequiredError('This feed requires auth') + } const { limit, cursor } = params const candidates: SkeletonFeedPost[] = [ { post: sc.posts[sc.dids.alice][0].ref.uriStr },