From 92bd5230743ab0bf4d22650136716b3fe8c5ab04 Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Thu, 5 Mar 2026 17:11:54 +0100 Subject: [PATCH] fix(topics): enrich author profile in by-author-rkey endpoints (#140) (#140) The by-author-rkey endpoints for both topics and replies returned raw authorDid without resolving the author profile, regressing the fix from #138 which only covered by-rkey and by-uri. The frontend switched to by-author-rkey in #177, causing DID display instead of profile info. --- src/routes/replies.ts | 14 ++++++++++- src/routes/topics.ts | 13 +++++++++- tests/unit/routes/replies.test.ts | 36 ++++++++++++++++++++++++++ tests/unit/routes/topics.test.ts | 42 +++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/routes/replies.ts b/src/routes/replies.ts index c49e156..862513b 100644 --- a/src/routes/replies.ts +++ b/src/routes/replies.ts @@ -741,7 +741,19 @@ export function replyRoutes(): FastifyPluginCallback { throw notFound('Reply not found') } - return reply.status(200).send(serializeReply(row)) + const serialized = serializeReply(row) + const communityDid = requireCommunityDid(request) + const authorMap = await resolveAuthors([row.authorDid], communityDid, db) + + return reply.status(200).send({ + ...serialized, + author: authorMap.get(row.authorDid) ?? { + did: row.authorDid, + handle: row.authorDid, + displayName: null, + avatarUrl: null, + }, + }) } ) diff --git a/src/routes/topics.ts b/src/routes/topics.ts index 219b814..12bf757 100644 --- a/src/routes/topics.ts +++ b/src/routes/topics.ts @@ -921,7 +921,18 @@ export function topicRoutes(): FastifyPluginCallback { throw forbidden('Content restricted by maturity settings') } - return reply.status(200).send(serializeTopic(row, categoryRating)) + const serialized = serializeTopic(row, categoryRating) + const authorMap = await resolveAuthors([row.authorDid], communityDid, db) + + return reply.status(200).send({ + ...serialized, + author: authorMap.get(row.authorDid) ?? { + did: row.authorDid, + handle: row.authorDid, + displayName: null, + avatarUrl: null, + }, + }) } ) diff --git a/tests/unit/routes/replies.test.ts b/tests/unit/routes/replies.test.ts index 4e44135..5beffd3 100644 --- a/tests/unit/routes/replies.test.ts +++ b/tests/unit/routes/replies.test.ts @@ -2617,6 +2617,42 @@ describe('reply routes', () => { expect(response.statusCode).toBe(404) }) + it('enriches author profile in by-author-rkey response', async () => { + resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) + const row = sampleReplyRow() + // 1. find reply by authorDid + rkey + selectChain.where.mockResolvedValueOnce([row]) + // 2. resolveAuthors: users table + selectChain.where.mockResolvedValueOnce([ + { + did: TEST_DID, + handle: TEST_HANDLE, + displayName: 'Jay', + avatarUrl: 'https://cdn.example.com/jay.jpg', + bannerUrl: null, + bio: null, + }, + ]) + // 3. resolveAuthors: community profiles + selectChain.where.mockResolvedValueOnce([]) + + const response = await app.inject({ + method: 'GET', + url: `/api/replies/by-author-rkey/${TEST_HANDLE}/${TEST_REPLY_RKEY}`, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ + author: { did: string; handle: string; displayName: string; avatarUrl: string } + }>() + expect(body.author).toEqual({ + did: TEST_DID, + handle: TEST_HANDLE, + displayName: 'Jay', + avatarUrl: 'https://cdn.example.com/jay.jpg', + }) + }) + it('returns 404 when reply not found for author', async () => { resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) selectChain.where.mockResolvedValueOnce([]) diff --git a/tests/unit/routes/topics.test.ts b/tests/unit/routes/topics.test.ts index d327296..eef2321 100644 --- a/tests/unit/routes/topics.test.ts +++ b/tests/unit/routes/topics.test.ts @@ -1280,6 +1280,48 @@ describe('topic routes', () => { expect(response.statusCode).toBe(404) }) + it('enriches author profile in by-author-rkey response', async () => { + resolveHandleToDidFn.mockResolvedValueOnce(TEST_DID) + const row = sampleTopicRow() + // 1. find topic by authorDid + rkey + selectChain.where.mockResolvedValueOnce([row]) + // 2. category maturity rating + selectChain.where.mockResolvedValueOnce([{ maturityRating: 'safe' }]) + // 3. user profile (maturity) + selectChain.where.mockResolvedValueOnce([{ declaredAge: null, maturityPref: 'safe' }]) + // 4. age threshold + selectChain.where.mockResolvedValueOnce([{ ageThreshold: 16 }]) + // 5. resolveAuthors: users table + selectChain.where.mockResolvedValueOnce([ + { + did: TEST_DID, + handle: TEST_HANDLE, + displayName: 'Jay', + avatarUrl: 'https://cdn.example.com/jay.jpg', + bannerUrl: null, + bio: null, + }, + ]) + // 6. resolveAuthors: community profiles + selectChain.where.mockResolvedValueOnce([]) + + const response = await app.inject({ + method: 'GET', + url: `/api/topics/by-author-rkey/${TEST_HANDLE}/${TEST_RKEY}`, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ + author: { did: string; handle: string; displayName: string; avatarUrl: string } + }>() + expect(body.author).toEqual({ + did: TEST_DID, + handle: TEST_HANDLE, + displayName: 'Jay', + avatarUrl: 'https://cdn.example.com/jay.jpg', + }) + }) + it('returns 403 when maturity blocks access', async () => { const noAuthApp = await buildTestApp(undefined) -- 2.51.2