Files
Chris Troutner e02bea5460 Implement topic recency ordering and pagination
- Indexer maintains topicSummaries and topicRecency for topic messages and
  follows, idempotently, moving each room's recency record to its newest post.
- DB adds the two index stores and serves GET /topics from them with
  limit/offset pagination, without iterating the rooms store. Adds
  util/room/backfill-topic-indexes.js to build the indexes idempotently.
- Client topics page loads 50 per page with Previous/Next.

By coder.
2026-09-17 07:24:45 -07:00

137 lines
4.2 KiB
JavaScript

import { assert } from 'chai'
import sinon from 'sinon'
import TopicsRESTControllerLib from '../../../src/controllers/rest-api/topics/controller.js'
describe('#TopicsRESTController', () => {
let uut
let sandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new TopicsRESTControllerLib({
adapters: {},
useCases: {
listTopics: {
execute: sandbox.stub().resolves({
topics: [
{ room: 'bitcoin', postCount: 2 },
{ room: 'cash', postCount: 1 }
]
})
},
listTopicPosts: {
execute: sandbox.stub().resolves({
posts: [{ txid: 'post-300', blockHeight: 300 }],
pagination: { limit: 100, offset: 0, total: 1, hasMore: false }
})
},
topicFollowState: {
execute: sandbox.stub().resolves({
room: 'bitcoin',
addr: 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d',
following: true
})
},
listTopicFollowers: {
execute: sandbox.stub().resolves({
room: 'bitcoin',
followers: ['bitcoincash:a', 'bitcoincash:b']
})
}
}
})
})
afterEach(() => sandbox.restore())
it('should return topics from use case', async () => {
const ctx = { query: { limit: '2', offset: '4' }, body: null, throw: sandbox.stub() }
await uut.getTopics(ctx)
assert.equal(uut.useCases.listTopics.execute.callCount, 1)
assert.deepEqual(uut.useCases.listTopics.execute.firstCall.args[0], { limit: '2', offset: '4' })
assert.equal(ctx.body.topics.length, 2)
assert.equal(ctx.body.topics[0].room, 'bitcoin')
})
it('should return topic posts from use case', async () => {
const ctx = {
params: { room: 'bitcoin' },
query: { limit: '50', offset: '0' },
body: null,
throw: sandbox.stub()
}
await uut.getTopicPosts(ctx)
assert.equal(uut.useCases.listTopicPosts.execute.callCount, 1)
assert.deepEqual(uut.useCases.listTopicPosts.execute.firstCall.args[0], {
room: 'bitcoin',
limit: '50',
offset: '0'
})
assert.equal(ctx.body.posts.length, 1)
assert.equal(ctx.body.posts[0].txid, 'post-300')
})
it('should return topic follow state from use case', async () => {
const ctx = {
params: { room: 'bitcoin' },
query: { addr: 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' },
body: null,
throw: sandbox.stub()
}
await uut.getTopicFollowState(ctx)
assert.equal(uut.useCases.topicFollowState.execute.callCount, 1)
assert.deepEqual(uut.useCases.topicFollowState.execute.firstCall.args[0], {
room: 'bitcoin',
addr: 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
})
assert.equal(ctx.body.following, true)
})
it('should return topic followers from use case', async () => {
const ctx = {
params: { room: 'bitcoin' },
body: null,
throw: sandbox.stub()
}
await uut.getTopicFollowers(ctx)
assert.equal(uut.useCases.listTopicFollowers.execute.callCount, 1)
assert.deepEqual(uut.useCases.listTopicFollowers.execute.firstCall.args[0], {
room: 'bitcoin'
})
assert.deepEqual(ctx.body.followers, ['bitcoincash:a', 'bitcoincash:b'])
})
it('should throw a 500 when the use case fails without a status', async () => {
uut.useCases.listTopics.execute = sandbox.stub().rejects(new Error('boom'))
const ctx = { query: {}, body: null, throw: sandbox.stub() }
await uut.getTopics(ctx)
assert.equal(ctx.throw.callCount, 1)
assert.equal(ctx.throw.firstCall.args[0], 500)
assert.equal(ctx.throw.firstCall.args[1], 'boom')
})
it('should preserve the status when the use case throws a statused error', async () => {
const err = new Error('room is required')
err.status = 400
uut.useCases.listTopicPosts.execute = sandbox.stub().rejects(err)
const ctx = {
params: {},
query: {},
body: null,
throw: sandbox.stub()
}
await uut.getTopicPosts(ctx)
assert.equal(ctx.throw.callCount, 1)
assert.equal(ctx.throw.firstCall.args[0], 400)
assert.equal(ctx.throw.firstCall.args[1], 'room is required')
})
})