Review and harden following feed: kill survivors, DRY parseRequiredString

- Extract parseRequiredString into pagination.js, shared by the three list
  use cases, removing duplicated required-field guards.
- Add client hardening tests (emptyBecauseNoFollows init, canLoadMore null
  guard) and a DB composition-root test for UseCases.start.
- Refresh mutation manifests and Gherkin acceptance-mutation stamp.

By architect.
This commit is contained in:
Chris Troutner
2026-09-02 12:20:37 -07:00
parent 62d6297197
commit 4bf99e7db0
13 changed files with 121 additions and 39 deletions
+1 -1
View File
@@ -345,5 +345,5 @@ class PostQuery {
export default PostQuery
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T14:30:59.518Z","module_hash":"48f80ca7bfd456c9cf4c373e5ed22db42826d44df7a720babbd4e6b8e80a8e76","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":14,"end_line":59,"hash":"d7104ee46a40c256bc93c795c04a71f9ff212d723480cbc75ec6d497a0995e84"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":61,"end_line":63,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":65,"end_line":67,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.addrPostHeightKey","name":"PostQuery.addrPostHeightKey","line":69,"end_line":71,"hash":"48579f08593cee36cc2f107c2526ac9def687b354cc5e54089defa3fd37117eb"},{"id":"func/PostQuery.postLikeKey","name":"PostQuery.postLikeKey","line":73,"end_line":75,"hash":"5d16caac2cf88932702b28f9d95927183f8904948eb8c16c7e8c672cd3a0780c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":77,"end_line":80,"hash":"0fd135c51089dcc9bd2f166bb9f28faaa6a03d7eb84cf03cb9b36e97cdb3139c"},{"id":"func/PostQuery.txidFromAddrPostHeight","name":"PostQuery.txidFromAddrPostHeight","line":82,"end_line":84,"hash":"314d5432292a78b273e3165343d3e09276600cbaf239f76bcd1b36fe5fcf5e10"},{"id":"func/PostQuery.txidFromKeyParts","name":"PostQuery.txidFromKeyParts","line":87,"end_line":90,"hash":"59fb8173599070095d87e5d6f56eeb999f79e75e4d3f3e435515e9db8ac71868"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":92,"end_line":94,"hash":"74621495a3affc6ef8688b9d6a814a91c99b22c86c348d261c952aad25df1661"},{"id":"func/PostQuery.isReply","name":"PostQuery.isReply","line":96,"end_line":104,"hash":"be2e3729bd5f05cbfbab3630678eb5c389bd04c616d53b1e0c48c852f4cb25b1"},{"id":"func/PostQuery.countRepliesForTxids","name":"PostQuery.countRepliesForTxids","line":107,"end_line":121,"hash":"16268cd2a0f8db020a099b704c3d5a3cea5daf6a0936dd1f8d85c38809f006aa"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":124,"end_line":134,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.countLikesForTxids","name":"PostQuery.countLikesForTxids","line":137,"end_line":152,"hash":"54e6e3e6467e72d9dd033c9861b3b9ef5e426a76aed7a13e8ac1a0f0389468a3"},{"id":"func/PostQuery.likeTxidFromPostLike","name":"PostQuery.likeTxidFromPostLike","line":154,"end_line":158,"hash":"7e07a9c278a9abae8f646b4f1950f88760f2d5c6445600a42fa42f36d029a45a"},{"id":"func/PostQuery.buildLikeCountMap","name":"PostQuery.buildLikeCountMap","line":162,"end_line":174,"hash":"a82ca3b46d68426edbe25f176c4a6019ea5fb6abea4c5d5e84aff3b381f63c61"},{"id":"func/PostQuery.postTxidFromPostLike","name":"PostQuery.postTxidFromPostLike","line":176,"end_line":180,"hash":"da7f8d0c63dbc074fb25da1a31dde5075c2c3469b84a5c700bd4a2961879d8e9"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":183,"end_line":190,"hash":"792ce2a8d5f19ed3d159c7af7e95c310e5b0c05cbef4de5be8f8f78403680b91"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":194,"end_line":202,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":204,"end_line":219,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxidsAndCount","name":"PostQuery.scanPostsByAddrTxidsAndCount","line":225,"end_line":253,"hash":"a872d7a1f71ce1ba6a1dee46a820989cbbb7287dfc433051be1e15890dbc010b"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":256,"end_line":259,"hash":"3498f2b9615e79b9472a5c7be26520c78db7c1661071c90a882f981d8acca477"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":261,"end_line":277,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":279,"end_line":290,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":293,"end_line":308,"hash":"6e12c0b66758cd0103ff6abad1379b3883bea82f084d2382991b4643dfb63360"}]}
// {"version":1,"tested_at":"2026-09-02T19:03:53.926Z","module_hash":"19d494a556bfcad4d1f49ed37ab693fea343df3c5e9161702905c852872f08d7","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":14,"end_line":60,"hash":"1588b3f2716a69f4470536875b0e7a0ef21d34ff6fb9312a2772e71f22d44c2c"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":62,"end_line":64,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":66,"end_line":68,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.addrPostHeightKey","name":"PostQuery.addrPostHeightKey","line":70,"end_line":72,"hash":"48579f08593cee36cc2f107c2526ac9def687b354cc5e54089defa3fd37117eb"},{"id":"func/PostQuery.postLikeKey","name":"PostQuery.postLikeKey","line":74,"end_line":76,"hash":"5d16caac2cf88932702b28f9d95927183f8904948eb8c16c7e8c672cd3a0780c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":78,"end_line":81,"hash":"0fd135c51089dcc9bd2f166bb9f28faaa6a03d7eb84cf03cb9b36e97cdb3139c"},{"id":"func/PostQuery.txidFromAddrPostHeight","name":"PostQuery.txidFromAddrPostHeight","line":83,"end_line":85,"hash":"314d5432292a78b273e3165343d3e09276600cbaf239f76bcd1b36fe5fcf5e10"},{"id":"func/PostQuery.txidFromKeyParts","name":"PostQuery.txidFromKeyParts","line":88,"end_line":91,"hash":"59fb8173599070095d87e5d6f56eeb999f79e75e4d3f3e435515e9db8ac71868"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":93,"end_line":95,"hash":"74621495a3affc6ef8688b9d6a814a91c99b22c86c348d261c952aad25df1661"},{"id":"func/PostQuery.isReply","name":"PostQuery.isReply","line":97,"end_line":105,"hash":"be2e3729bd5f05cbfbab3630678eb5c389bd04c616d53b1e0c48c852f4cb25b1"},{"id":"func/PostQuery.countRepliesForTxids","name":"PostQuery.countRepliesForTxids","line":108,"end_line":122,"hash":"16268cd2a0f8db020a099b704c3d5a3cea5daf6a0936dd1f8d85c38809f006aa"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":125,"end_line":135,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.countLikesForTxids","name":"PostQuery.countLikesForTxids","line":138,"end_line":153,"hash":"54e6e3e6467e72d9dd033c9861b3b9ef5e426a76aed7a13e8ac1a0f0389468a3"},{"id":"func/PostQuery.likeTxidFromPostLike","name":"PostQuery.likeTxidFromPostLike","line":155,"end_line":159,"hash":"7e07a9c278a9abae8f646b4f1950f88760f2d5c6445600a42fa42f36d029a45a"},{"id":"func/PostQuery.buildLikeCountMap","name":"PostQuery.buildLikeCountMap","line":163,"end_line":175,"hash":"a82ca3b46d68426edbe25f176c4a6019ea5fb6abea4c5d5e84aff3b381f63c61"},{"id":"func/PostQuery.postTxidFromPostLike","name":"PostQuery.postTxidFromPostLike","line":177,"end_line":181,"hash":"da7f8d0c63dbc074fb25da1a31dde5075c2c3469b84a5c700bd4a2961879d8e9"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":184,"end_line":191,"hash":"792ce2a8d5f19ed3d159c7af7e95c310e5b0c05cbef4de5be8f8f78403680b91"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":195,"end_line":203,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":205,"end_line":220,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxidsAndCount","name":"PostQuery.scanPostsByAddrTxidsAndCount","line":226,"end_line":254,"hash":"a872d7a1f71ce1ba6a1dee46a820989cbbb7287dfc433051be1e15890dbc010b"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":257,"end_line":260,"hash":"3498f2b9615e79b9472a5c7be26520c78db7c1661071c90a882f981d8acca477"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":262,"end_line":278,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":280,"end_line":291,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":294,"end_line":309,"hash":"6e12c0b66758cd0103ff6abad1379b3883bea82f084d2382991b4643dfb63360"},{"id":"func/PostQuery.scanFollowingFeedTxidsAndCount","name":"PostQuery.scanFollowingFeedTxidsAndCount","line":314,"end_line":342,"hash":"90920dc6ed0d082545edc8b860f2f6fcc13e632510b9ddf879a4e4c7efa0e652"}]}
// mutate4javascript-manifest-end
@@ -154,3 +154,7 @@ class PostsRESTControllerLib {
}
export default PostsRESTControllerLib
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-02T19:04:12.781Z","module_hash":"e9f06f6fdf475b85ec8ebd378f9c8784f38d0d05254d463f98b6df37f58c2886","functions":[{"id":"func/PostsRESTControllerLib.constructor","name":"PostsRESTControllerLib.constructor","line":8,"end_line":25,"hash":"ffe914078c8e659d1dc86fac2c236f5310aeddebffbf01c48d3c1907b2adb902"},{"id":"func/PostsRESTControllerLib.runUseCase","name":"PostsRESTControllerLib.runUseCase","line":31,"end_line":37,"hash":"116112d778cc231815bd7ac296e8e3f90fbc209e1c91892e9bec596ed0821863"},{"id":"func/PostsRESTControllerLib.listPostsForAddr","name":"PostsRESTControllerLib.listPostsForAddr","line":44,"end_line":48,"hash":"23c4480bc080ea2da6ca30cc36b78cc3b51357bf2cdb5927c1511c430b89e0c9"},{"id":"func/PostsRESTControllerLib.handleError","name":"PostsRESTControllerLib.handleError","line":50,"end_line":57,"hash":"57720513b9edacb2e38e6089ae5881c73cdc767c941977d8d979d2bf37513965"},{"id":"func/PostsRESTControllerLib.getRecentPosts","name":"PostsRESTControllerLib.getRecentPosts","line":86,"end_line":89,"hash":"cb96f5a491bc3f52e71ed5ff1bbc5ce1654bb38ec86dca0f00b407778645c322"},{"id":"func/PostsRESTControllerLib.getPostsByAddr","name":"PostsRESTControllerLib.getPostsByAddr","line":115,"end_line":117,"hash":"41283c2ead5c5015962daa1c094ed52db593750eb01094ce4dbe74c6ea979b70"},{"id":"func/PostsRESTControllerLib.getFollowingFeed","name":"PostsRESTControllerLib.getFollowingFeed","line":144,"end_line":146,"hash":"bba784a60d63043e039b341a6affb3029c1b1259f26479f078be93fef4fd6d3c"},{"id":"func/PostsRESTControllerLib.getPostThread","name":"PostsRESTControllerLib.getPostThread","line":148,"end_line":153,"hash":"d4a7873e7e024b861c7e1aebc98b4ebb563eab80d21f0027c94bda46fc9a20fb"}]}
// mutate4javascript-manifest-end
@@ -34,3 +34,7 @@ class PostsRouter {
}
export default PostsRouter
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-02T19:04:31.503Z","module_hash":"640e7871f89083654c3a3d442b91cd29bc5f9f483f43bc3f521e7c874932a696","functions":[{"id":"func/PostsRouter.constructor","name":"PostsRouter.constructor","line":9,"end_line":24,"hash":"552bd82e3f00dd57053a773a52a72c40e522cb146e146bf3335cc119504e963e"},{"id":"func/PostsRouter.attach","name":"PostsRouter.attach","line":26,"end_line":33,"hash":"452b2740b90a4f51ac1b4fdde8c305b6a5e6e93e806acd4e5d37d2e9b940bbf1"}]}
// mutate4javascript-manifest-end
+1 -1
View File
@@ -133,5 +133,5 @@ class UseCases {
export default UseCases
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-29T03:34:22.727Z","module_hash":"bd1af3666615a359eb970d1576472fe7a792fbfd135972e81ba50f1441976e94","functions":[{"id":"func/UseCases.constructor","name":"UseCases.constructor","line":23,"end_line":48,"hash":"511ff53d6fe3084c42bc443e67183991e065fe6432d26291d8f44fcf3ee3fe94"},{"id":"func/UseCases.start","name":"UseCases.start","line":50,"end_line":118,"hash":"4a94f92a0c335ee536da858df44d13e81930b86df9e27ebff75d71753865e299"}]}
// {"version":1,"tested_at":"2026-09-02T19:03:33.402Z","module_hash":"c2846cfbc10b257fb2bc78b233698bcb3629774248ed55c01936745fa6542537","functions":[{"id":"func/UseCases.constructor","name":"UseCases.constructor","line":25,"end_line":52,"hash":"4c06a996b9753331ed29694f0c1939617e0da1a4255244aecd7fa2f4eb4521f3"},{"id":"func/UseCases.start","name":"UseCases.start","line":54,"end_line":130,"hash":"358ac54d6f521176f500c42ce8577b5f2fcc9051b6ae095e4d5b07cc35a7b81a"}]}
// mutate4javascript-manifest-end
+10 -1
View File
@@ -43,6 +43,15 @@ export function parseOffset (offset) {
return parsed
}
// Validate a required string field (e.g. an address or room) shared by the
// addr- and room-scoped list use cases.
export function parseRequiredString (value, fieldName) {
if (!value || typeof value !== 'string') {
throw httpError(`${fieldName} is required`, 400)
}
return value
}
export function attachReplyCounts (posts, replyCounts) {
return posts.map((post) => ({
...post,
@@ -72,5 +81,5 @@ export function assemblePostPage ({ posts, replyCounts, likeCounts, total, limit
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T03:32:37.566Z","module_hash":"e13c6a9aac8093c65559ec81bc44239611f47270c9199a8b4dd8fc67b36a0e5f","functions":[{"id":"func/isEmpty","name":"isEmpty","line":13,"end_line":15,"hash":"209ecce14d6de3b7500065dd3091a7f2c006d1b08305078720ddd2ab250bc501"},{"id":"func/httpError","name":"httpError","line":17,"end_line":21,"hash":"270f69e0007e397fbd6beac1f2fd7d3a1156f9ba1cdf2d230e4e867c8b221aeb"},{"id":"func/parseLimit","name":"parseLimit","line":23,"end_line":34,"hash":"20c64d382dedbd2b65e68d66a35ccba8ecfc78dc32534a697e3865f05021eaf2"},{"id":"func/parseOffset","name":"parseOffset","line":36,"end_line":44,"hash":"55b13c976e2d53e6b69658555d97f4e63d425b5cb6b7b47b57081bd18ef16845"},{"id":"func/attachReplyCounts","name":"attachReplyCounts","line":46,"end_line":51,"hash":"99b9e818ac032752eb76fc9fcdcba0dcd1d94bee830921131a18fd01a4848f76"},{"id":"func/attachLikeCounts","name":"attachLikeCounts","line":53,"end_line":58,"hash":"eebdaa4235d209f5ab3cfb7c242bb76b29c0365676107eee7f681ecb1c188ea8"},{"id":"func/assemblePostPage","name":"assemblePostPage","line":60,"end_line":72,"hash":"496cbbf9f16d3b36ffbe486a62462cbb939f02b9f1610e4a577d2e5ff167c51f"}]}
// {"version":1,"tested_at":"2026-09-02T19:03:14.637Z","module_hash":"31610c64eb700d95f93b85fd90d35ef9c34ec4ad386478534134db76cd227171","functions":[{"id":"func/isEmpty","name":"isEmpty","line":13,"end_line":15,"hash":"209ecce14d6de3b7500065dd3091a7f2c006d1b08305078720ddd2ab250bc501"},{"id":"func/httpError","name":"httpError","line":17,"end_line":21,"hash":"270f69e0007e397fbd6beac1f2fd7d3a1156f9ba1cdf2d230e4e867c8b221aeb"},{"id":"func/parseLimit","name":"parseLimit","line":23,"end_line":34,"hash":"20c64d382dedbd2b65e68d66a35ccba8ecfc78dc32534a697e3865f05021eaf2"},{"id":"func/parseOffset","name":"parseOffset","line":36,"end_line":44,"hash":"55b13c976e2d53e6b69658555d97f4e63d425b5cb6b7b47b57081bd18ef16845"},{"id":"func/parseRequiredString","name":"parseRequiredString","line":48,"end_line":53,"hash":"0073c76aefb8129d225c86da88824381da8e9f6c956b5d2fa01485b65c83ea47"},{"id":"func/attachReplyCounts","name":"attachReplyCounts","line":55,"end_line":60,"hash":"99b9e818ac032752eb76fc9fcdcba0dcd1d94bee830921131a18fd01a4848f76"},{"id":"func/attachLikeCounts","name":"attachLikeCounts","line":62,"end_line":67,"hash":"eebdaa4235d209f5ab3cfb7c242bb76b29c0365676107eee7f681ecb1c188ea8"},{"id":"func/assemblePostPage","name":"assemblePostPage","line":69,"end_line":81,"hash":"496cbbf9f16d3b36ffbe486a62462cbb939f02b9f1610e4a577d2e5ff167c51f"}]}
// mutate4javascript-manifest-end
@@ -5,7 +5,7 @@
viewer's own posts are excluded. Results are paginated with limit/offset.
*/
import { parseLimit, parseOffset, assemblePostPage } from './lib/pagination.js'
import { parseLimit, parseOffset, parseRequiredString, assemblePostPage } from './lib/pagination.js'
import { ListUseCase } from './lib/use-case.js'
class ListFollowingFeed extends ListUseCase {
@@ -16,17 +16,8 @@ class ListFollowingFeed extends ListUseCase {
}
}
parseAddr (addr) {
if (!addr || typeof addr !== 'string') {
const err = new Error('addr is required')
err.status = 400
throw err
}
return addr
}
async execute (inObj = {}) {
const addr = this.parseAddr(inObj.addr)
const addr = parseRequiredString(inObj.addr, 'addr')
const limit = parseLimit(inObj.limit)
const offset = parseOffset(inObj.offset)
@@ -47,3 +38,7 @@ class ListFollowingFeed extends ListUseCase {
}
export default ListFollowingFeed
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-09-02T19:02:18.559Z","module_hash":"df53fbf714d93da7638df954aba33dd2605b4ce42a8f09fe0ea11d32c3f65eb6","functions":[{"id":"func/ListFollowingFeed.constructor","name":"ListFollowingFeed.constructor","line":12,"end_line":17,"hash":"b62fd6f9f182ccdd9797918d8d662d1c753d24c019a0401a0011c76296112a6d"},{"id":"func/ListFollowingFeed.execute","name":"ListFollowingFeed.execute","line":19,"end_line":37,"hash":"c32d42cb050f69164bab44febe375ba73acfa2c8b39659c7e84c4bbc4146c17e"}]}
// mutate4javascript-manifest-end
@@ -3,7 +3,7 @@
Uses the postHeights secondary index for efficient sorting and pagination.
*/
import { parseLimit, parseOffset, assemblePostPage } from './lib/pagination.js'
import { parseLimit, parseOffset, parseRequiredString, assemblePostPage } from './lib/pagination.js'
import { ListUseCase } from './lib/use-case.js'
class ListPostsByAddr extends ListUseCase {
@@ -11,17 +11,8 @@ class ListPostsByAddr extends ListUseCase {
super(localConfig, { useCaseName: 'ListPostsByAddr', adapterName: 'postQuery' })
}
parseAddr (addr) {
if (!addr || typeof addr !== 'string') {
const err = new Error('addr is required')
err.status = 400
throw err
}
return addr
}
async execute (inObj = {}) {
const addr = this.parseAddr(inObj.addr)
const addr = parseRequiredString(inObj.addr, 'addr')
const limit = parseLimit(inObj.limit)
const offset = parseOffset(inObj.offset)
@@ -39,5 +30,5 @@ class ListPostsByAddr extends ListUseCase {
export default ListPostsByAddr
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T04:26:54.668Z","module_hash":"fb2f5f9d5132a1fba8d76483cf2cf6aa1e7478bb143d8eaba55789b0af39c8c6","functions":[{"id":"func/ListPostsByAddr.constructor","name":"ListPostsByAddr.constructor","line":10,"end_line":12,"hash":"f777f3685c5b2199ec3c3a7043b3cf288a9bbf583232bd1fc58cc346373f03d6"},{"id":"func/ListPostsByAddr.parseAddr","name":"ListPostsByAddr.parseAddr","line":14,"end_line":21,"hash":"0f02fc6ebf05826429d57bf2fd64bf66dfde96ee159f6c1d4814f41cc7a25aca"},{"id":"func/ListPostsByAddr.execute","name":"ListPostsByAddr.execute","line":23,"end_line":36,"hash":"1275dac9f67add375fb09ec3b2e202217a03ead211bbd27bcff4607df5b7462e"}]}
// {"version":1,"tested_at":"2026-09-02T19:02:37.206Z","module_hash":"ca00e44ac02115d358d25a2932fe1084f39070c5754a9e96c27a37e215a5998e","functions":[{"id":"func/ListPostsByAddr.constructor","name":"ListPostsByAddr.constructor","line":10,"end_line":12,"hash":"f777f3685c5b2199ec3c3a7043b3cf288a9bbf583232bd1fc58cc346373f03d6"},{"id":"func/ListPostsByAddr.execute","name":"ListPostsByAddr.execute","line":14,"end_line":27,"hash":"c21b84d10ac7308f026f5967c6b1f2113357f8f3c96f600c38e2fc08e64f7caf"}]}
// mutate4javascript-manifest-end
+3 -12
View File
@@ -3,7 +3,7 @@
(newest first), paginated.
*/
import { parseLimit, parseOffset, attachReplyCounts, attachLikeCounts } from './lib/pagination.js'
import { parseLimit, parseOffset, parseRequiredString, attachReplyCounts, attachLikeCounts } from './lib/pagination.js'
class ListTopicPosts {
constructor (localConfig = {}) {
@@ -21,17 +21,8 @@ class ListTopicPosts {
this.execute = this.execute.bind(this)
}
parseRoom (room) {
if (!room || typeof room !== 'string') {
const err = new Error('room is required')
err.status = 400
throw err
}
return room
}
async execute (inObj = {}) {
const room = this.parseRoom(inObj.room)
const room = parseRequiredString(inObj.room, 'room')
const limit = parseLimit(inObj.limit)
const offset = parseOffset(inObj.offset)
@@ -58,5 +49,5 @@ class ListTopicPosts {
export default ListTopicPosts
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-28T15:16:11.032Z","module_hash":"42a65ea5f1a4dea88cd1c99bcf8ba8b476dc24a073459ffa26d21151179db078","functions":[{"id":"func/ListTopicPosts.constructor","name":"ListTopicPosts.constructor","line":9,"end_line":22,"hash":"cdc313ae9059c07ce5cd2440e49fc958b9d273ed5ab43b0a6b81fd4949ad9272"},{"id":"func/ListTopicPosts.parseRoom","name":"ListTopicPosts.parseRoom","line":24,"end_line":31,"hash":"6508b101a3e1c0a32fed8f54f5b0383f2a891e5aa5c59a6fe0d1af6c9ffb9f37"},{"id":"func/ListTopicPosts.execute","name":"ListTopicPosts.execute","line":33,"end_line":55,"hash":"3ed269937f0d908694ba94f2faef658b92d7160be1262abffcd268fe022c352c"}]}
// {"version":1,"tested_at":"2026-09-02T19:02:55.018Z","module_hash":"fc9d576550fdf3097c37feae14a9c1d7a8d15e44fa7c705140d5c869bc2a4f02","functions":[{"id":"func/ListTopicPosts.constructor","name":"ListTopicPosts.constructor","line":9,"end_line":22,"hash":"cdc313ae9059c07ce5cd2440e49fc958b9d273ed5ab43b0a6b81fd4949ad9272"},{"id":"func/ListTopicPosts.execute","name":"ListTopicPosts.execute","line":24,"end_line":46,"hash":"61f3386dac6823688ea4d0a6dbf3edebc77b36d3171e8a2661274d8871dcc39a"}]}
// mutate4javascript-manifest-end
@@ -0,0 +1,60 @@
/*
Unit tests for the UseCases composition root.
*/
import { assert } from 'chai'
import UseCases from '../../../src/use-cases/index.js'
describe('#UseCases', () => {
// Build a minimal adapters bundle that satisfies every use-case constructor
// so start() can instantiate the full set.
function makeAdapters () {
return {
postQuery: {},
profileQuery: {},
followQuery: {
listFollowing: () => [],
listFollowers: () => []
},
muteQuery: {
listMuted: () => []
},
topicQuery: {},
searchQuery: {},
pollQuery: {}
}
}
describe('#constructor', () => {
it('throws when adapters are missing', () => {
assert.throws(() => new UseCases({}), /Adapters required/)
})
})
describe('#start', () => {
it('instantiates every use case and returns true', async () => {
const uut = new UseCases({ adapters: makeAdapters() })
const result = await uut.start()
assert.equal(result, true)
assert.isNotNull(uut.listRecentProfiles)
assert.isNotNull(uut.listRecentPosts)
assert.isNotNull(uut.listPostsByAddr)
assert.isNotNull(uut.listFollowingFeed)
assert.isNotNull(uut.getPostThread)
assert.isNotNull(uut.followState)
assert.isNotNull(uut.listFollowing)
assert.isNotNull(uut.listFollowers)
assert.isNotNull(uut.muteState)
assert.isNotNull(uut.listMuted)
assert.isNotNull(uut.listTopics)
assert.isNotNull(uut.listTopicPosts)
assert.isNotNull(uut.topicFollowState)
assert.isNotNull(uut.listTopicFollowers)
assert.isNotNull(uut.getPoll)
assert.isNotNull(uut.getPollOptions)
assert.isNotNull(uut.getPollVotes)
assert.isNotNull(uut.searchAll)
})
})
})