mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Review notifications refactor: extract shared getPostOrNull, add adapter unit suite
Extract the post-lookup-with-null helper into a shared module used by both the notifications and post query adapters, removing duplication. Add a dedicated NotificationsQuery unit suite (26 tests) and hardening tests that kill the sort-comparator and followee-guard mutations. Refresh mutation manifests via the approved tool. Document 5 intrinsic mutation equivalents and 16 soft-Gherkin equivalents in the review summary. By architect.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Shared helper to fetch a post by txid, returning null when not found.
|
||||
|
||||
Both the post query and notifications query adapters need to look up a post
|
||||
by txid and treat a missing record as null. Centralizing the lookup keeps
|
||||
not-found behavior identical across adapters.
|
||||
*/
|
||||
|
||||
export async function getPostOrNull (postsDb, txid) {
|
||||
try {
|
||||
return await postsDb.get(txid)
|
||||
} catch (err) {
|
||||
if (err.notFound || err.code === 'LEVEL_NOT_FOUND') return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-03T21:02:50.881Z","module_hash":"dc90dc5717937cb47da000846b351e798c679e4b6149762f357b14a705db01b9","functions":[{"id":"func/getPostOrNull","name":"getPostOrNull","line":9,"end_line":16,"hash":"f633a6b98ca1a0e21b8c544e53a37744d4a2ebc6e5d2c9dba71a3fe951d77220"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import BCHJS from '@psf/bch-js'
|
||||
import { getPostOrNull } from './lib/get-post-or-null.js'
|
||||
|
||||
class NotificationsQuery {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -48,7 +49,6 @@ class NotificationsQuery {
|
||||
this.bchjs = bchjs
|
||||
|
||||
this.listNotifications = this.listNotifications.bind(this)
|
||||
this._getPostOrNull = this._getPostOrNull.bind(this)
|
||||
this._collectFollowNotifications = this._collectFollowNotifications.bind(this)
|
||||
this._collectLikeNotifications = this._collectLikeNotifications.bind(this)
|
||||
this._collectReplyNotifications = this._collectReplyNotifications.bind(this)
|
||||
@@ -56,15 +56,6 @@ class NotificationsQuery {
|
||||
this._sortNotifications = this._sortNotifications.bind(this)
|
||||
}
|
||||
|
||||
async _getPostOrNull (txid) {
|
||||
try {
|
||||
return await this.postsDb.get(txid)
|
||||
} catch (err) {
|
||||
if (err.notFound || err.code === 'LEVEL_NOT_FOUND') return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Collect active follows where this address is the followee.
|
||||
async _collectFollowNotifications (addr) {
|
||||
const myHash160 = this.bchjs.Address.toHash160(addr)
|
||||
@@ -96,7 +87,7 @@ class NotificationsQuery {
|
||||
for await (const [likeTxid, like] of this.likesDb.iterator()) {
|
||||
if (!like || like.addr === addr) continue
|
||||
|
||||
const post = await this._getPostOrNull(like.postTxid)
|
||||
const post = await getPostOrNull(this.postsDb, like.postTxid)
|
||||
if (!post || post.addr !== addr) continue
|
||||
|
||||
notifications.push({
|
||||
@@ -144,10 +135,10 @@ class NotificationsQuery {
|
||||
// the child is missing or authored by addr. Callers must have already
|
||||
// verified the record carries parentTxid and childTxid.
|
||||
async _replyNotificationChild (child, addr) {
|
||||
const parent = await this._getPostOrNull(child.parentTxid)
|
||||
const parent = await getPostOrNull(this.postsDb, child.parentTxid)
|
||||
if (!parent || parent.addr !== addr) return null
|
||||
|
||||
const childPost = await this._getPostOrNull(child.childTxid)
|
||||
const childPost = await getPostOrNull(this.postsDb, child.childTxid)
|
||||
if (!childPost || childPost.addr === addr) return null
|
||||
|
||||
return childPost
|
||||
@@ -177,5 +168,5 @@ class NotificationsQuery {
|
||||
export default NotificationsQuery
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-02T20:02:58.120Z","module_hash":"1a298fb4ce5a783162b6e34e8840073f97e0f74c1dc29c675e4d8954ea6c7ae6","functions":[{"id":"func/NotificationsQuery.constructor","name":"NotificationsQuery.constructor","line":12,"end_line":57,"hash":"962099cdc7a50291b0998e09081f53d6d1ee88cdabda016b9318476ce6129507"},{"id":"func/NotificationsQuery._getPostOrNull","name":"NotificationsQuery._getPostOrNull","line":59,"end_line":66,"hash":"5457ca98de35d7ad6569365f4061c56fdb1495db9d2429f267c5c9fee3f2bb00"},{"id":"func/NotificationsQuery._collectFollowNotifications","name":"NotificationsQuery._collectFollowNotifications","line":69,"end_line":90,"hash":"702150793e1b80639ff125864d0841f8037a83701b1773e5e41451cc087d2995"},{"id":"func/NotificationsQuery._collectLikeNotifications","name":"NotificationsQuery._collectLikeNotifications","line":93,"end_line":113,"hash":"1ec11eb62288a1353a74dfd21f8e01635aca72764b01afe033b57c06b713d5d0"},{"id":"func/NotificationsQuery._collectReplyNotifications","name":"NotificationsQuery._collectReplyNotifications","line":116,"end_line":139,"hash":"ab0acd3f37231a52065caee1728913abaa8c9d154d1a7068f743588521a94d4b"},{"id":"func/NotificationsQuery._replyNotificationChild","name":"NotificationsQuery._replyNotificationChild","line":146,"end_line":154,"hash":"b45fbb877dc7fabd01da40362fe5ab92e2729d4f86cf2a27ac129a5f37bb0e5e"},{"id":"func/NotificationsQuery._sortNotifications","name":"NotificationsQuery._sortNotifications","line":156,"end_line":161,"hash":"61b4ae0b6d450e172db3e59240a3c2ccd5fdf0e6ea1e31023df7bb3d52fdbd5c"},{"id":"func/NotificationsQuery.listNotifications","name":"NotificationsQuery.listNotifications","line":164,"end_line":174,"hash":"02d410897747d5e7a7c7ab1df393dfef13e61569ea53442922786ffd7f172554"}]}
|
||||
// {"version":1,"tested_at":"2026-09-03T20:56:26.158Z","module_hash":"99eb9f1bc59e4fe3331ab1058437c7b3d4fa7fea42fc9997174d0eb68c7b5f58","functions":[{"id":"func/NotificationsQuery.constructor","name":"NotificationsQuery.constructor","line":13,"end_line":57,"hash":"278a2ce990616f369eb5f038721f1acaca4a0b9525e8f5ab346a073eeb7bf25b"},{"id":"func/NotificationsQuery._collectFollowNotifications","name":"NotificationsQuery._collectFollowNotifications","line":60,"end_line":81,"hash":"702150793e1b80639ff125864d0841f8037a83701b1773e5e41451cc087d2995"},{"id":"func/NotificationsQuery._collectLikeNotifications","name":"NotificationsQuery._collectLikeNotifications","line":84,"end_line":104,"hash":"7054e1a936cd1a9006c93fd23fe7f32873aa7ce4a3efce9defb55d72e6bc3384"},{"id":"func/NotificationsQuery._collectReplyNotifications","name":"NotificationsQuery._collectReplyNotifications","line":107,"end_line":130,"hash":"ab0acd3f37231a52065caee1728913abaa8c9d154d1a7068f743588521a94d4b"},{"id":"func/NotificationsQuery._replyNotificationChild","name":"NotificationsQuery._replyNotificationChild","line":137,"end_line":145,"hash":"7ed6bf9d9f5147ba189c99304b76a1a0d77bc9c7909e194f9b74545c84227ed0"},{"id":"func/NotificationsQuery._sortNotifications","name":"NotificationsQuery._sortNotifications","line":147,"end_line":152,"hash":"61b4ae0b6d450e172db3e59240a3c2ccd5fdf0e6ea1e31023df7bb3d52fdbd5c"},{"id":"func/NotificationsQuery.listNotifications","name":"NotificationsQuery.listNotifications","line":155,"end_line":165,"hash":"02d410897747d5e7a7c7ab1df393dfef13e61569ea53442922786ffd7f172554"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { loadReplyTxids } from './lib/load-reply-txids.js'
|
||||
import { getPostOrNull as getPostOrNullShared } from './lib/get-post-or-null.js'
|
||||
|
||||
const HEIGHT_PAD = 12
|
||||
|
||||
@@ -183,12 +184,7 @@ class PostQuery {
|
||||
|
||||
// Fetch a post by txid, returning null when the post is not found.
|
||||
async getPostOrNull (txid) {
|
||||
try {
|
||||
return await this.postsDb.get(txid)
|
||||
} catch (err) {
|
||||
if (err.notFound || err.code === 'LEVEL_NOT_FOUND') return null
|
||||
throw err
|
||||
}
|
||||
return getPostOrNullShared(this.postsDb, txid)
|
||||
}
|
||||
|
||||
// Iterate the txids of top-level posts (replies excluded) in postHeights
|
||||
@@ -352,5 +348,5 @@ class PostQuery {
|
||||
export default PostQuery
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-02T19:43:15.045Z","module_hash":"0868137cfaf51799185994217d4ea07263c8a2043ad1f50ed01626e9e3009175","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":14,"end_line":61,"hash":"ad258234c9efee42e33489f859824240cd39f759a4d9168a4fa42aaa76fe96ec"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":63,"end_line":65,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":67,"end_line":69,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.addrPostHeightKey","name":"PostQuery.addrPostHeightKey","line":71,"end_line":73,"hash":"48579f08593cee36cc2f107c2526ac9def687b354cc5e54089defa3fd37117eb"},{"id":"func/PostQuery.postLikeKey","name":"PostQuery.postLikeKey","line":75,"end_line":77,"hash":"5d16caac2cf88932702b28f9d95927183f8904948eb8c16c7e8c672cd3a0780c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":79,"end_line":82,"hash":"0fd135c51089dcc9bd2f166bb9f28faaa6a03d7eb84cf03cb9b36e97cdb3139c"},{"id":"func/PostQuery.txidFromAddrPostHeight","name":"PostQuery.txidFromAddrPostHeight","line":84,"end_line":86,"hash":"314d5432292a78b273e3165343d3e09276600cbaf239f76bcd1b36fe5fcf5e10"},{"id":"func/PostQuery.txidFromKeyParts","name":"PostQuery.txidFromKeyParts","line":89,"end_line":92,"hash":"59fb8173599070095d87e5d6f56eeb999f79e75e4d3f3e435515e9db8ac71868"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":94,"end_line":96,"hash":"74621495a3affc6ef8688b9d6a814a91c99b22c86c348d261c952aad25df1661"},{"id":"func/PostQuery.isReply","name":"PostQuery.isReply","line":98,"end_line":106,"hash":"be2e3729bd5f05cbfbab3630678eb5c389bd04c616d53b1e0c48c852f4cb25b1"},{"id":"func/PostQuery.countRepliesForTxids","name":"PostQuery.countRepliesForTxids","line":109,"end_line":123,"hash":"16268cd2a0f8db020a099b704c3d5a3cea5daf6a0936dd1f8d85c38809f006aa"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":126,"end_line":136,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.countLikesForTxids","name":"PostQuery.countLikesForTxids","line":139,"end_line":154,"hash":"54e6e3e6467e72d9dd033c9861b3b9ef5e426a76aed7a13e8ac1a0f0389468a3"},{"id":"func/PostQuery.likeTxidFromPostLike","name":"PostQuery.likeTxidFromPostLike","line":156,"end_line":160,"hash":"7e07a9c278a9abae8f646b4f1950f88760f2d5c6445600a42fa42f36d029a45a"},{"id":"func/PostQuery.buildLikeCountMap","name":"PostQuery.buildLikeCountMap","line":164,"end_line":176,"hash":"a82ca3b46d68426edbe25f176c4a6019ea5fb6abea4c5d5e84aff3b381f63c61"},{"id":"func/PostQuery.postTxidFromPostLike","name":"PostQuery.postTxidFromPostLike","line":178,"end_line":182,"hash":"da7f8d0c63dbc074fb25da1a31dde5075c2c3469b84a5c700bd4a2961879d8e9"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":185,"end_line":192,"hash":"792ce2a8d5f19ed3d159c7af7e95c310e5b0c05cbef4de5be8f8f78403680b91"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":196,"end_line":204,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":206,"end_line":221,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxidsAndCount","name":"PostQuery.scanPostsByAddrTxidsAndCount","line":227,"end_line":255,"hash":"a872d7a1f71ce1ba6a1dee46a820989cbbb7287dfc433051be1e15890dbc010b"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":258,"end_line":261,"hash":"3498f2b9615e79b9472a5c7be26520c78db7c1661071c90a882f981d8acca477"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":263,"end_line":279,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":281,"end_line":292,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":295,"end_line":310,"hash":"6e12c0b66758cd0103ff6abad1379b3883bea82f084d2382991b4643dfb63360"},{"id":"func/PostQuery.scanFollowingFeedTxidsAndCount","name":"PostQuery.scanFollowingFeedTxidsAndCount","line":315,"end_line":339,"hash":"1ef4b7fb555d82c33971d3e0a4d2f630c42eb86261a78cd34765a5e3736e5946"},{"id":"func/PostQuery.isFolloweePost","name":"PostQuery.isFolloweePost","line":344,"end_line":349,"hash":"7825f4509ef5103d2340ecb1434b483b02c7b435a263777ca89e4a0d345f2be3"}]}
|
||||
// {"version":1,"tested_at":"2026-09-03T20:59:14.298Z","module_hash":"bc540f6e0c6ef34850afd8637499ec459000a0ac73c57060b88e63f1f28e52bc","functions":[{"id":"func/PostQuery.constructor","name":"PostQuery.constructor","line":15,"end_line":62,"hash":"ad258234c9efee42e33489f859824240cd39f759a4d9168a4fa42aaa76fe96ec"},{"id":"func/PostQuery.padHeight","name":"PostQuery.padHeight","line":64,"end_line":66,"hash":"be6c442a4d3d86ab3b60314756b7f7c0592479c21cb3b2e1273dcf139a84fb00"},{"id":"func/PostQuery.postHeightKey","name":"PostQuery.postHeightKey","line":68,"end_line":70,"hash":"2d4dff9464aa4c1e805da5de2ba314fbd856530c046705237ea848d5feff8c7c"},{"id":"func/PostQuery.addrPostHeightKey","name":"PostQuery.addrPostHeightKey","line":72,"end_line":74,"hash":"48579f08593cee36cc2f107c2526ac9def687b354cc5e54089defa3fd37117eb"},{"id":"func/PostQuery.postLikeKey","name":"PostQuery.postLikeKey","line":76,"end_line":78,"hash":"5d16caac2cf88932702b28f9d95927183f8904948eb8c16c7e8c672cd3a0780c"},{"id":"func/PostQuery.txidFromPostHeight","name":"PostQuery.txidFromPostHeight","line":80,"end_line":83,"hash":"0fd135c51089dcc9bd2f166bb9f28faaa6a03d7eb84cf03cb9b36e97cdb3139c"},{"id":"func/PostQuery.txidFromAddrPostHeight","name":"PostQuery.txidFromAddrPostHeight","line":85,"end_line":87,"hash":"314d5432292a78b273e3165343d3e09276600cbaf239f76bcd1b36fe5fcf5e10"},{"id":"func/PostQuery.txidFromKeyParts","name":"PostQuery.txidFromKeyParts","line":90,"end_line":93,"hash":"59fb8173599070095d87e5d6f56eeb999f79e75e4d3f3e435515e9db8ac71868"},{"id":"func/PostQuery.loadReplyTxids","name":"PostQuery.loadReplyTxids","line":95,"end_line":97,"hash":"74621495a3affc6ef8688b9d6a814a91c99b22c86c348d261c952aad25df1661"},{"id":"func/PostQuery.isReply","name":"PostQuery.isReply","line":99,"end_line":107,"hash":"be2e3729bd5f05cbfbab3630678eb5c389bd04c616d53b1e0c48c852f4cb25b1"},{"id":"func/PostQuery.countRepliesForTxids","name":"PostQuery.countRepliesForTxids","line":110,"end_line":124,"hash":"16268cd2a0f8db020a099b704c3d5a3cea5daf6a0936dd1f8d85c38809f006aa"},{"id":"func/PostQuery.buildReplyCountMap","name":"PostQuery.buildReplyCountMap","line":127,"end_line":137,"hash":"1d9762d70dca3439c0bb382b09882faee215f1d53f0656aff8bcbde429ec63b4"},{"id":"func/PostQuery.countLikesForTxids","name":"PostQuery.countLikesForTxids","line":140,"end_line":155,"hash":"54e6e3e6467e72d9dd033c9861b3b9ef5e426a76aed7a13e8ac1a0f0389468a3"},{"id":"func/PostQuery.likeTxidFromPostLike","name":"PostQuery.likeTxidFromPostLike","line":157,"end_line":161,"hash":"7e07a9c278a9abae8f646b4f1950f88760f2d5c6445600a42fa42f36d029a45a"},{"id":"func/PostQuery.buildLikeCountMap","name":"PostQuery.buildLikeCountMap","line":165,"end_line":177,"hash":"a82ca3b46d68426edbe25f176c4a6019ea5fb6abea4c5d5e84aff3b381f63c61"},{"id":"func/PostQuery.postTxidFromPostLike","name":"PostQuery.postTxidFromPostLike","line":179,"end_line":183,"hash":"da7f8d0c63dbc074fb25da1a31dde5075c2c3469b84a5c700bd4a2961879d8e9"},{"id":"func/PostQuery.getPostOrNull","name":"PostQuery.getPostOrNull","line":186,"end_line":188,"hash":"d06ce38cd8bde722482a749ff58740b174a054900519e67d2045f29a18fce3f7"},{"id":"func/PostQuery.topLevelPostTxids","name":"PostQuery.topLevelPostTxids","line":192,"end_line":200,"hash":"0457d8b43b692d7bbfde283e63663d74542778d3893b45202fcb6ae0f1fc6776"},{"id":"func/PostQuery.scanRecentPostTxids","name":"PostQuery.scanRecentPostTxids","line":202,"end_line":217,"hash":"bed887c0eeec051e082657cac518bbd2f60df84bbb86c9d8aa76015194e7a73a"},{"id":"func/PostQuery.scanPostsByAddrTxidsAndCount","name":"PostQuery.scanPostsByAddrTxidsAndCount","line":223,"end_line":251,"hash":"a872d7a1f71ce1ba6a1dee46a820989cbbb7287dfc433051be1e15890dbc010b"},{"id":"func/PostQuery.scanPostsByAddrTxids","name":"PostQuery.scanPostsByAddrTxids","line":254,"end_line":257,"hash":"3498f2b9615e79b9472a5c7be26520c78db7c1661071c90a882f981d8acca477"},{"id":"func/PostQuery.loadPostsByTxids","name":"PostQuery.loadPostsByTxids","line":259,"end_line":275,"hash":"86b05107f3ecc240b2e734217cedf29ef44c48474bf31c1c8f75f2e4b4f84bea"},{"id":"func/PostQuery.countTopLevelPosts","name":"PostQuery.countTopLevelPosts","line":277,"end_line":288,"hash":"a26a6fdd12201de71965545f4113e3598f325fa4d96076289d371e4157c667ff"},{"id":"func/PostQuery.countTopLevelPostsByAddr","name":"PostQuery.countTopLevelPostsByAddr","line":291,"end_line":306,"hash":"6e12c0b66758cd0103ff6abad1379b3883bea82f084d2382991b4643dfb63360"},{"id":"func/PostQuery.scanFollowingFeedTxidsAndCount","name":"PostQuery.scanFollowingFeedTxidsAndCount","line":311,"end_line":335,"hash":"1ef4b7fb555d82c33971d3e0a4d2f630c42eb86261a78cd34765a5e3736e5946"},{"id":"func/PostQuery.isFolloweePost","name":"PostQuery.isFolloweePost","line":340,"end_line":345,"hash":"7825f4509ef5103d2340ecb1434b483b02c7b435a263777ca89e4a0d345f2be3"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -185,5 +185,5 @@ 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"}]}
|
||||
// {"version":1,"tested_at":"2026-09-02T20:14:56.373Z","module_hash":"5d43fd45fdfeeef08e0c69af9f1e4e06128dd1858818c0cd38eb718619083f58","functions":[{"id":"func/PostsRESTControllerLib.constructor","name":"PostsRESTControllerLib.constructor","line":8,"end_line":26,"hash":"8d7ff84be53037a92bd5834179c065a5edf05facd70778c2ed35aa871f1f9e40"},{"id":"func/PostsRESTControllerLib.runUseCase","name":"PostsRESTControllerLib.runUseCase","line":32,"end_line":38,"hash":"116112d778cc231815bd7ac296e8e3f90fbc209e1c91892e9bec596ed0821863"},{"id":"func/PostsRESTControllerLib.listPostsForAddr","name":"PostsRESTControllerLib.listPostsForAddr","line":45,"end_line":49,"hash":"23c4480bc080ea2da6ca30cc36b78cc3b51357bf2cdb5927c1511c430b89e0c9"},{"id":"func/PostsRESTControllerLib.handleError","name":"PostsRESTControllerLib.handleError","line":51,"end_line":58,"hash":"57720513b9edacb2e38e6089ae5881c73cdc767c941977d8d979d2bf37513965"},{"id":"func/PostsRESTControllerLib.getRecentPosts","name":"PostsRESTControllerLib.getRecentPosts","line":87,"end_line":90,"hash":"cb96f5a491bc3f52e71ed5ff1bbc5ce1654bb38ec86dca0f00b407778645c322"},{"id":"func/PostsRESTControllerLib.getPostsByAddr","name":"PostsRESTControllerLib.getPostsByAddr","line":116,"end_line":118,"hash":"41283c2ead5c5015962daa1c094ed52db593750eb01094ce4dbe74c6ea979b70"},{"id":"func/PostsRESTControllerLib.getFollowingFeed","name":"PostsRESTControllerLib.getFollowingFeed","line":145,"end_line":147,"hash":"bba784a60d63043e039b341a6affb3029c1b1259f26479f078be93fef4fd6d3c"},{"id":"func/PostsRESTControllerLib.getNotifications","name":"PostsRESTControllerLib.getNotifications","line":173,"end_line":175,"hash":"3385ba48da47021b6f8770f643d4d37c5092f79f674736f3ee04adaf3c951092"},{"id":"func/PostsRESTControllerLib.getPostThread","name":"PostsRESTControllerLib.getPostThread","line":177,"end_line":182,"hash":"d4a7873e7e024b861c7e1aebc98b4ebb563eab80d21f0027c94bda46fc9a20fb"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -37,5 +37,5 @@ 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"}]}
|
||||
// {"version":1,"tested_at":"2026-09-02T20:15:32.177Z","module_hash":"57e7ca39a57e35a97b547b683a77223d17d666ba6878f08a387e42d044a46f85","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":34,"hash":"f6d97a26736967d4be3685c63cfcae0eab7001d27dca22ce5c71199e98209d36"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -139,5 +139,5 @@ class UseCases {
|
||||
export default UseCases
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"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"}]}
|
||||
// {"version":1,"tested_at":"2026-09-02T20:16:09.198Z","module_hash":"246be2cf01feda067a62a77a8502f76cef8569cb63dff49bf7b139fdd25dd355","functions":[{"id":"func/UseCases.constructor","name":"UseCases.constructor","line":26,"end_line":54,"hash":"e8f3118c89d65ad1abddc4ad92dd13c7b77ae063767be248339f581a7651749e"},{"id":"func/UseCases.start","name":"UseCases.start","line":56,"end_line":136,"hash":"3207d2a7848465e421e0bdc61719cf4aa61b23e1dcdf751af087f13e6dc984cb"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -35,5 +35,5 @@ class ListNotifications extends ListUseCase {
|
||||
export default ListNotifications
|
||||
|
||||
// mutate4javascript-manifest-begin
|
||||
// {"version":1,"tested_at":"2026-09-02T19:56:06.917Z","module_hash":"9cee2e299acbb1203a40425d6d429eec8b1e3978f67fb0f3e76f5c32644d956c","functions":[{"id":"func/ListNotifications.constructor","name":"ListNotifications.constructor","line":12,"end_line":14,"hash":"84b86af8804c5aa8b25ec55f1190863b4652d4460d3d25f0cde0fdad2efda18f"},{"id":"func/ListNotifications.execute","name":"ListNotifications.execute","line":16,"end_line":32,"hash":"59384e928f105a7995ff014b410e0832f812a306c4598e19cfe18e9be570d0f0"}]}
|
||||
// {"version":1,"tested_at":"2026-09-02T20:14:19.388Z","module_hash":"9cee2e299acbb1203a40425d6d429eec8b1e3978f67fb0f3e76f5c32644d956c","functions":[{"id":"func/ListNotifications.constructor","name":"ListNotifications.constructor","line":12,"end_line":14,"hash":"84b86af8804c5aa8b25ec55f1190863b4652d4460d3d25f0cde0fdad2efda18f"},{"id":"func/ListNotifications.execute","name":"ListNotifications.execute","line":16,"end_line":32,"hash":"59384e928f105a7995ff014b410e0832f812a306c4598e19cfe18e9be570d0f0"}]}
|
||||
// mutate4javascript-manifest-end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import NotificationsQuery from '../../../src/adapters/notifications-query.js'
|
||||
import { getPostOrNull } from '../../../src/adapters/lib/get-post-or-null.js'
|
||||
|
||||
describe('#NotificationsQuery', () => {
|
||||
let sandbox
|
||||
@@ -92,6 +93,49 @@ describe('#NotificationsQuery', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw when postParentsDb is missing', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new NotificationsQuery({ postsDb, postChildrenDb, likesDb, postLikesDb: {}, followsDb, bchjs })
|
||||
assert.fail('Expected error')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'postParentsDb required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw when postLikesDb is missing', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new NotificationsQuery({ postsDb, postParentsDb: {}, postChildrenDb, likesDb, followsDb, bchjs })
|
||||
assert.fail('Expected error')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'postLikesDb required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return null from getPostOrNull when the post is not found', async () => {
|
||||
const missingTxid = 'c'.repeat(64)
|
||||
const notFound = new Error('not found')
|
||||
notFound.notFound = true
|
||||
postsDb.get.withArgs(missingTxid).rejects(notFound)
|
||||
|
||||
const result = await getPostOrNull(postsDb, missingTxid)
|
||||
assert.equal(result, null)
|
||||
})
|
||||
|
||||
it('should rethrow non-notFound errors from getPostOrNull', async () => {
|
||||
const txid = 'd'.repeat(64)
|
||||
const boom = new Error('boom')
|
||||
postsDb.get.withArgs(txid).rejects(boom)
|
||||
|
||||
try {
|
||||
await getPostOrNull(postsDb, txid)
|
||||
assert.fail('Expected error')
|
||||
} catch (err) {
|
||||
assert.equal(err, boom)
|
||||
}
|
||||
})
|
||||
|
||||
it('should include a reply to my post', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
const replyTxid = 'b'.repeat(64)
|
||||
@@ -191,6 +235,18 @@ describe('#NotificationsQuery', () => {
|
||||
assert.equal(result.total, 0)
|
||||
})
|
||||
|
||||
it('should exclude follows of other people', async () => {
|
||||
postChildrenDb.iterator.returns(makeIterator([]))
|
||||
likesDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([
|
||||
[`${THEIR_ADDR}:${'otherhash'}`, { followerAddr: THEIR_ADDR, followeePkHash: 'otherhash', unfollow: false, txid: 'c'.repeat(64), blockHeight: 150 }]
|
||||
]))
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.total, 0)
|
||||
})
|
||||
|
||||
it('should exclude unfollows', async () => {
|
||||
postChildrenDb.iterator.returns(makeIterator([]))
|
||||
likesDb.iterator.returns(makeIterator([]))
|
||||
@@ -226,6 +282,35 @@ describe('#NotificationsQuery', () => {
|
||||
assert.equal(result.notifications[1].type, 'reply')
|
||||
})
|
||||
|
||||
it('should sort three notifications by block height descending', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
const replyTxid = 'b'.repeat(64)
|
||||
const likeTxid = 'c'.repeat(64)
|
||||
|
||||
postChildrenDb.iterator.returns(makeIterator([
|
||||
[`${myPostTxid}:${replyTxid}`, { parentTxid: myPostTxid, childTxid: replyTxid, blockHeight: 200 }]
|
||||
]))
|
||||
likesDb.iterator.returns(makeIterator([
|
||||
[likeTxid, { addr: THEIR_ADDR, postTxid: myPostTxid, blockHeight: 300 }]
|
||||
]))
|
||||
followsDb.iterator.returns(makeIterator([
|
||||
[`${THEIR_ADDR}:${MY_HASH160}`, { followerAddr: THEIR_ADDR, followeePkHash: MY_HASH160, unfollow: false, txid: 'd'.repeat(64), blockHeight: 100 }]
|
||||
]))
|
||||
|
||||
postsDb.get.withArgs(myPostTxid).resolves({ addr: MY_ADDR, text: 'hello' })
|
||||
postsDb.get.withArgs(replyTxid).resolves({ addr: THEIR_ADDR, text: 'reply' })
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.total, 3)
|
||||
assert.equal(result.notifications[0].type, 'like')
|
||||
assert.equal(result.notifications[0].blockHeight, 300)
|
||||
assert.equal(result.notifications[1].type, 'reply')
|
||||
assert.equal(result.notifications[1].blockHeight, 200)
|
||||
assert.equal(result.notifications[2].type, 'follow')
|
||||
assert.equal(result.notifications[2].blockHeight, 100)
|
||||
})
|
||||
|
||||
it('should paginate notifications', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
const replyTxid = 'b'.repeat(64)
|
||||
@@ -248,4 +333,141 @@ describe('#NotificationsQuery', () => {
|
||||
assert.equal(result.notifications.length, 1)
|
||||
assert.equal(result.notifications[0].type, 'like')
|
||||
})
|
||||
|
||||
it('should default follow blockHeight and seen to 0 when missing', async () => {
|
||||
postChildrenDb.iterator.returns(makeIterator([]))
|
||||
likesDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([
|
||||
[`${THEIR_ADDR}:${MY_HASH160}`, { followerAddr: THEIR_ADDR, followeePkHash: MY_HASH160, unfollow: false, txid: 'c'.repeat(64) }]
|
||||
]))
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.notifications.length, 1)
|
||||
const n = result.notifications[0]
|
||||
assert.equal(n.type, 'follow')
|
||||
assert.equal(n.blockHeight, 0)
|
||||
assert.equal(n.seen, 0)
|
||||
})
|
||||
|
||||
it('should default like blockHeight and seen to 0 when missing', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
const likeTxid = 'b'.repeat(64)
|
||||
|
||||
postChildrenDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([]))
|
||||
likesDb.iterator.returns(makeIterator([
|
||||
[likeTxid, { addr: THEIR_ADDR, postTxid: myPostTxid }]
|
||||
]))
|
||||
postsDb.get.withArgs(myPostTxid).resolves({ addr: MY_ADDR, text: 'hello' })
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.notifications.length, 1)
|
||||
const n = result.notifications[0]
|
||||
assert.equal(n.type, 'like')
|
||||
assert.equal(n.blockHeight, 0)
|
||||
assert.equal(n.seen, 0)
|
||||
})
|
||||
|
||||
it('should default reply blockHeight and seen to 0 when missing', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
const replyTxid = 'b'.repeat(64)
|
||||
|
||||
postChildrenDb.iterator.returns(makeIterator([
|
||||
[`${myPostTxid}:${replyTxid}`, { parentTxid: myPostTxid, childTxid: replyTxid }]
|
||||
]))
|
||||
likesDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([]))
|
||||
postsDb.get.withArgs(myPostTxid).resolves({ addr: MY_ADDR, text: 'hello' })
|
||||
postsDb.get.withArgs(replyTxid).resolves({ addr: THEIR_ADDR, text: 'reply' })
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.notifications.length, 1)
|
||||
const n = result.notifications[0]
|
||||
assert.equal(n.type, 'reply')
|
||||
assert.equal(n.blockHeight, 0)
|
||||
assert.equal(n.seen, 0)
|
||||
})
|
||||
|
||||
it('should use the child post blockHeight when the child record lacks one', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
const replyTxid = 'b'.repeat(64)
|
||||
|
||||
postChildrenDb.iterator.returns(makeIterator([
|
||||
[`${myPostTxid}:${replyTxid}`, { parentTxid: myPostTxid, childTxid: replyTxid }]
|
||||
]))
|
||||
likesDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([]))
|
||||
postsDb.get.withArgs(myPostTxid).resolves({ addr: MY_ADDR, text: 'hello' })
|
||||
postsDb.get.withArgs(replyTxid).resolves({ addr: THEIR_ADDR, text: 'reply', blockHeight: 500, seen: 7 })
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.notifications.length, 1)
|
||||
const n = result.notifications[0]
|
||||
assert.equal(n.blockHeight, 500)
|
||||
assert.equal(n.seen, 7)
|
||||
})
|
||||
|
||||
it('should break sort ties by seen descending when blockHeight is equal', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
|
||||
postChildrenDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([]))
|
||||
likesDb.iterator.returns(makeIterator([
|
||||
['b1'.repeat(32), { addr: THEIR_ADDR, postTxid: myPostTxid, blockHeight: 300, seen: 100 }],
|
||||
['b2'.repeat(32), { addr: 'bitcoincash:qother2', postTxid: myPostTxid, blockHeight: 300, seen: 200 }]
|
||||
]))
|
||||
postsDb.get.withArgs(myPostTxid).resolves({ addr: MY_ADDR, text: 'hello' })
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.notifications.length, 2)
|
||||
assert.equal(result.notifications[0].seen, 200)
|
||||
assert.equal(result.notifications[1].seen, 100)
|
||||
})
|
||||
|
||||
it('should skip a like whose post is missing', async () => {
|
||||
const myPostTxid = 'a'.repeat(64)
|
||||
const likeTxid = 'b'.repeat(64)
|
||||
const missingPost = new Error('not found')
|
||||
missingPost.notFound = true
|
||||
|
||||
postChildrenDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([]))
|
||||
likesDb.iterator.returns(makeIterator([
|
||||
[likeTxid, { addr: THEIR_ADDR, postTxid: myPostTxid, blockHeight: 300 }]
|
||||
]))
|
||||
postsDb.get.withArgs(myPostTxid).rejects(missingPost)
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.total, 0)
|
||||
})
|
||||
|
||||
it('should skip a null like record without throwing', async () => {
|
||||
postChildrenDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([]))
|
||||
likesDb.iterator.returns(makeIterator([
|
||||
[null, null]
|
||||
]))
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.total, 0)
|
||||
})
|
||||
|
||||
it('should skip a reply child record missing parentTxid or childTxid', async () => {
|
||||
postChildrenDb.iterator.returns(makeIterator([
|
||||
['missing:child', { parentTxid: undefined, childTxid: 'b'.repeat(64), blockHeight: 100 }]
|
||||
]))
|
||||
likesDb.iterator.returns(makeIterator([]))
|
||||
followsDb.iterator.returns(makeIterator([]))
|
||||
|
||||
const result = await uut.listNotifications(MY_ADDR, { limit: 100, offset: 0 })
|
||||
|
||||
assert.equal(result.total, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user