Review and harden follow-user: kill mutation survivors

Add unit test pinning FollowQuery._nextString exclusive upper bound and
empty-string/non-string validation-rejection cases for the follow use
cases, killing the remaining mutation survivors. Update mutation manifests
and add the follow-user architectural review report.

By architect.
This commit is contained in:
Chris Troutner
2026-08-27 11:23:01 -07:00
parent b99d881205
commit bb7b5a6cfd
15 changed files with 116 additions and 2 deletions
@@ -1,3 +1,7 @@
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-08-27T18:18:35.249787308Z","feature_name":"Follow User","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/follow-user.feature","background_hash":"e1d5f81f1ed083ac6934c429ca3cb4a0f8d4dac44c2eaa45c0960920bde2c017","implementation_hash":"unknown","scenarios":[]}
# acceptance-mutation-manifest-end
# Scenarios: Follow User - 1, Follow User - 2, Follow User - 3, Follow User - 4, Follow User - 5
#
# The follow/unfollow OP_RETURN payload is the followee's 20-byte hash160.
+4
View File
@@ -107,3 +107,7 @@ class MemoDb {
}
export default MemoDb
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T17:52:51.766Z","module_hash":"3f694a85efbd3d3d6fb3d24c8261166d36113990091680e5d11485bef973d6c9","functions":[{"id":"func/MemoDb.constructor","name":"MemoDb.constructor","line":9,"end_line":11,"hash":"188825ae5983840d4e1a4df182966ef012297c336ea3b763c87ea1c384654279"},{"id":"func/MemoDb.getRecentProfiles","name":"MemoDb.getRecentProfiles","line":13,"end_line":15,"hash":"cb819c72881b70f719f964c607f64be70b40377297c738897eee495bcf6dff4d"},{"id":"func/MemoDb.getRecentPosts","name":"MemoDb.getRecentPosts","line":17,"end_line":19,"hash":"7c0fd29c2fdc67a27a05a190e2e4294a698a1fe5223d8017ca82c4f890c2d094"},{"id":"func/MemoDb.getProfile","name":"MemoDb.getProfile","line":21,"end_line":23,"hash":"6a250c7205799b40dbe97b8f17ddaee6eace8cb435b0561d9ff8f5eac93d2b20"},{"id":"func/MemoDb.getProfilePic","name":"MemoDb.getProfilePic","line":25,"end_line":27,"hash":"d61dd5aebce32ef28b69ad56f2f7108d6b5f333a4d94cd83f05f01a09fa8f1d7"},{"id":"func/MemoDb.getName","name":"MemoDb.getName","line":29,"end_line":31,"hash":"3f841735b93180c6aeeb14a563bb6d4eb0ca5d395fb960267c8896b06c1e85f3"},{"id":"func/MemoDb.getFollowState","name":"MemoDb.getFollowState","line":33,"end_line":49,"hash":"d62eb85c742b40f2d319230c6ef59e21738063d31bccf1f37ebdc52c20bac994"},{"id":"func/MemoDb.getRecent","name":"MemoDb.getRecent","line":52,"end_line":63,"hash":"1565fd17dced45fbf1b6d6fd6b8003521129bbd88046ff52a4eb61c17862530d"},{"id":"func/MemoDb.getLevelResource","name":"MemoDb.getLevelResource","line":66,"end_line":79,"hash":"c2fb7338918b1e69aa8a14f65ae687532c843f0641f04074947731691001e4ea"},{"id":"func/MemoDb.getPostsByAddr","name":"MemoDb.getPostsByAddr","line":81,"end_line":93,"hash":"c39322f75bec244cffd3b66614ae2814040ac428cf39af53b04d3fcae4d59b19"},{"id":"func/MemoDb.getPostThread","name":"MemoDb.getPostThread","line":95,"end_line":106,"hash":"ed5ef157b457f8d984ff6a9488a4dccf7a37b0ae65adb40005df90508426661f"}]}
// mutate4javascript-manifest-end
@@ -114,3 +114,7 @@ MemoFollow.MEMO_UNFOLLOW_PREFIX = MEMO_UNFOLLOW_PREFIX
MemoFollow.PK_HASH_LENGTH = PK_HASH_LENGTH
module.exports = MemoFollow
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T17:51:36.253Z","module_hash":"fc0d7db2a5f7872ae0d9228a19c7314b4f46f7574c53b2a168a62b986c43a31e","functions":[{"id":"func/MemoFollow.constructor","name":"MemoFollow.constructor","line":35,"end_line":38,"hash":"9407b43605444074011b1da595c9d53356352c72b0d847e00365d79ad705663a"},{"id":"func/MemoFollow.validate","name":"MemoFollow.validate","line":42,"end_line":57,"hash":"4154af442c36324ba147dff352c55610397fb430c9b62fbbe44728ab0e379179"},{"id":"func/MemoFollow.follow","name":"MemoFollow.follow","line":60,"end_line":62,"hash":"e24f69ef420d1ef625c2d03eae6014c824fdb35438a87e379fdc41747bbd321d"},{"id":"func/MemoFollow.unfollow","name":"MemoFollow.unfollow","line":65,"end_line":67,"hash":"48e93e85645bbcd767467afb807625b4adf6d6076f02fc841c91f58147bb44c7"},{"id":"func/MemoFollow._broadcastAction","name":"MemoFollow._broadcastAction","line":70,"end_line":88,"hash":"3720e2b6d78265383728bd7c5fcafeb018fad9c17a8bede12a6d4ffc3415cd3a"},{"id":"func/MemoFollow._toHash160","name":"MemoFollow._toHash160","line":92,"end_line":94,"hash":"3ed2ba4853b718a8316345c9f988ed3bed3ef647de3fd9761ee3c9e0bee86070"},{"id":"func/MemoFollow._ensureBchjs","name":"MemoFollow._ensureBchjs","line":96,"end_line":100,"hash":"92828d9038c9a4d367e80edd7f129291e434d57aae80b332081a66af9896a5d6"},{"id":"func/MemoFollow.reflect","name":"MemoFollow.reflect","line":104,"end_line":109,"hash":"af3f0ead1a9f51724e3c252122b3bf13c5323a3b9da244dee87a34815a90c94c"}]}
// mutate4javascript-manifest-end
+1 -1
View File
@@ -99,5 +99,5 @@ ProfilePage.PROFILE_PATH_PREFIX = PROFILE_PATH_PREFIX
module.exports = ProfilePage
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T17:41:02.811Z","module_hash":"556ad5297067de3e39ddbb028a87d502cc0a9ab6525b1bba2c3d36c669207e1d","functions":[{"id":"func/ProfilePage.constructor","name":"ProfilePage.constructor","line":17,"end_line":25,"hash":"5e8bd826d8059e2d86aa3064c7b7d3041d5cf2fff7eb8942976bda600d2812a2"},{"id":"func/ProfilePage.load","name":"ProfilePage.load","line":27,"end_line":41,"hash":"e6c81ab6dbfda80903b134bdfea626d7e82e567780fd577d7a78e6dd44a22f37"},{"id":"func/ProfilePage._assertReady","name":"ProfilePage._assertReady","line":44,"end_line":51,"hash":"e575f98fa1b492b4c11ae5275459d9f9996df4c566b84c7cae91413c72a02f38"},{"id":"func/ProfilePage._loadFollowState","name":"ProfilePage._loadFollowState","line":55,"end_line":60,"hash":"65ff871041cab36b5e45acad5c8ca904c411af197f77dbb528e31ac5666668b3"},{"id":"func/ProfilePage.isOwnProfile","name":"ProfilePage.isOwnProfile","line":62,"end_line":64,"hash":"4d149bfe7b183b5d38d496c0a8126d22bd9a919e684f375d66e2c8c1348dd773"},{"id":"func/ProfilePage.canFollow","name":"ProfilePage.canFollow","line":66,"end_line":68,"hash":"0bc2c0c17991d8f1a1d18fba29c901e42633b7cf2c2c8b5747a49e5832d7001d"},{"id":"func/ProfilePage.isFollowing","name":"ProfilePage.isFollowing","line":70,"end_line":72,"hash":"9fc0470db7ffea2da96d2dbbdceff55562e1f7e84377fbb0e8b60d53cc723b40"},{"id":"func/ProfilePage.follow","name":"ProfilePage.follow","line":74,"end_line":76,"hash":"7674b789a9d3c48e0f7a6e553613bac99b2f38449fcdd17faa3c6d7cd2773bdd"},{"id":"func/ProfilePage.unfollow","name":"ProfilePage.unfollow","line":78,"end_line":80,"hash":"69d278b09da1f284be6adacb7264f9c06ac71c42f8e8f1baf9e34d5b24a891fb"},{"id":"func/ProfilePage._setFollowState","name":"ProfilePage._setFollowState","line":83,"end_line":90,"hash":"8eda1a436468b17327e8e20df4f84bf0263591f320c494f2c74c1b5be21b2ad8"},{"id":"func/ProfilePage.getPost","name":"ProfilePage.getPost","line":92,"end_line":94,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"}]}
// {"version":1,"tested_at":"2026-08-27T17:52:10.673Z","module_hash":"556ad5297067de3e39ddbb028a87d502cc0a9ab6525b1bba2c3d36c669207e1d","functions":[{"id":"func/ProfilePage.constructor","name":"ProfilePage.constructor","line":17,"end_line":25,"hash":"5e8bd826d8059e2d86aa3064c7b7d3041d5cf2fff7eb8942976bda600d2812a2"},{"id":"func/ProfilePage.load","name":"ProfilePage.load","line":27,"end_line":41,"hash":"e6c81ab6dbfda80903b134bdfea626d7e82e567780fd577d7a78e6dd44a22f37"},{"id":"func/ProfilePage._assertReady","name":"ProfilePage._assertReady","line":44,"end_line":51,"hash":"e575f98fa1b492b4c11ae5275459d9f9996df4c566b84c7cae91413c72a02f38"},{"id":"func/ProfilePage._loadFollowState","name":"ProfilePage._loadFollowState","line":55,"end_line":60,"hash":"65ff871041cab36b5e45acad5c8ca904c411af197f77dbb528e31ac5666668b3"},{"id":"func/ProfilePage.isOwnProfile","name":"ProfilePage.isOwnProfile","line":62,"end_line":64,"hash":"4d149bfe7b183b5d38d496c0a8126d22bd9a919e684f375d66e2c8c1348dd773"},{"id":"func/ProfilePage.canFollow","name":"ProfilePage.canFollow","line":66,"end_line":68,"hash":"0bc2c0c17991d8f1a1d18fba29c901e42633b7cf2c2c8b5747a49e5832d7001d"},{"id":"func/ProfilePage.isFollowing","name":"ProfilePage.isFollowing","line":70,"end_line":72,"hash":"9fc0470db7ffea2da96d2dbbdceff55562e1f7e84377fbb0e8b60d53cc723b40"},{"id":"func/ProfilePage.follow","name":"ProfilePage.follow","line":74,"end_line":76,"hash":"7674b789a9d3c48e0f7a6e553613bac99b2f38449fcdd17faa3c6d7cd2773bdd"},{"id":"func/ProfilePage.unfollow","name":"ProfilePage.unfollow","line":78,"end_line":80,"hash":"69d278b09da1f284be6adacb7264f9c06ac71c42f8e8f1baf9e34d5b24a891fb"},{"id":"func/ProfilePage._setFollowState","name":"ProfilePage._setFollowState","line":83,"end_line":90,"hash":"8eda1a436468b17327e8e20df4f84bf0263591f320c494f2c74c1b5be21b2ad8"},{"id":"func/ProfilePage.getPost","name":"ProfilePage.getPost","line":92,"end_line":94,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"}]}
// mutate4javascript-manifest-end
+1 -1
View File
@@ -65,5 +65,5 @@ class Profiles {
module.exports = Profiles
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T15:05:55.871Z","module_hash":"7c75650f72fefd5e9f8fea638c1d9ca9fad9eeebabe16e62fed4f5e828e39248","functions":[{"id":"func/Profiles.constructor","name":"Profiles.constructor","line":13,"end_line":16,"hash":"8f7409de885759ce97767c69097a353ec557bd5212eef76ead0388174389ff24"},{"id":"func/Profiles.setName","name":"Profiles.setName","line":18,"end_line":21,"hash":"5a36c6e237798608de0bedd8744b75000c0eec5c0a6a64c870a25bcdf20aed21"},{"id":"func/Profiles.getName","name":"Profiles.getName","line":23,"end_line":26,"hash":"2fcb9d84687ea0f3b24f3e34c0a72eda55a4e869b87e08a7f4551321a4166198"},{"id":"func/Profiles.setBio","name":"Profiles.setBio","line":28,"end_line":31,"hash":"3825665ead1ba9bb217694c45a17bb2e11c78ffa053e5e90111a4b9208183b56"},{"id":"func/Profiles.getBio","name":"Profiles.getBio","line":33,"end_line":36,"hash":"2e9708249db4a0e4fa642cbe52e6216144ec91283281c61dee2ff3cc3d8f1572"}]}
// {"version":1,"tested_at":"2026-08-27T17:52:37.432Z","module_hash":"f729d4125eea29c45c4a0d7bd691a5f590512922f2ba78c85756643638fb0859","functions":[{"id":"func/Profiles.constructor","name":"Profiles.constructor","line":13,"end_line":18,"hash":"574f0b3772f783646c283c6ddc4d5ff200ca1f9918ab1fa668205003f885d4d3"},{"id":"func/Profiles.setName","name":"Profiles.setName","line":20,"end_line":23,"hash":"5a36c6e237798608de0bedd8744b75000c0eec5c0a6a64c870a25bcdf20aed21"},{"id":"func/Profiles.getName","name":"Profiles.getName","line":25,"end_line":28,"hash":"2fcb9d84687ea0f3b24f3e34c0a72eda55a4e869b87e08a7f4551321a4166198"},{"id":"func/Profiles.setBio","name":"Profiles.setBio","line":30,"end_line":33,"hash":"3825665ead1ba9bb217694c45a17bb2e11c78ffa053e5e90111a4b9208183b56"},{"id":"func/Profiles.getBio","name":"Profiles.getBio","line":35,"end_line":38,"hash":"2e9708249db4a0e4fa642cbe52e6216144ec91283281c61dee2ff3cc3d8f1572"},{"id":"func/Profiles.setAvatarUrl","name":"Profiles.setAvatarUrl","line":40,"end_line":43,"hash":"e4ccb0164fee4ab6cc3a1290fb4bf85e4a79f2227ca72872b1f10bc616926fd9"},{"id":"func/Profiles.getAvatarUrl","name":"Profiles.getAvatarUrl","line":45,"end_line":48,"hash":"0aa58dc15fd519e553136d04d1954e18084f79a6cd9349dc89b1136cf0a22a2b"},{"id":"func/Profiles.setFollowState","name":"Profiles.setFollowState","line":51,"end_line":57,"hash":"aa527936b6ca92a502e93e8241803318700c528e05f68ff7fb6fac1d410c197c"},{"id":"func/Profiles.getFollowState","name":"Profiles.getFollowState","line":59,"end_line":62,"hash":"299a44adb289026bf3b89a21947fc23bf05f5a40f07abb7f1bbd311c0479f562"}]}
// mutate4javascript-manifest-end
+4
View File
@@ -1,3 +1,7 @@
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-08-27T18:19:23.459992768Z","feature_name":"Follow Read","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-db/specs/follow-read.feature","background_hash":"d8fb430d3eb584b455ccd5d1b5cfbd99a494bc5c382c14df2af63f56369e1f2a","implementation_hash":"unknown","scenarios":[{"index":1,"name":"Follow Read - 2 GET /follow/following lists the addresses a follower follows","scenario_hash":"9a8847dcd27c43aa71377d4d887e8b5b74a7ad428d74e63e8decd6c96ea6a3c1","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-08-27T18:19:23.459992768Z"},{"index":2,"name":"Follow Read - 3 GET /follow/followers lists the addresses that follow a followee","scenario_hash":"7ac6ac4d7fa00d0c9ac1307631a1a11103ad241251dc325914e9cc3adba9b01a","mutation_count":4,"result":{"Total":4,"Killed":4,"Survived":0,"Errors":0},"tested_at":"2026-08-27T18:19:23.459992768Z"}]}
# acceptance-mutation-manifest-end
# Scenarios: Follow Read - 1, Follow Read - 2, Follow Read - 3
#
# The follows store keys followees by 20-byte hash160. Use bch-js
+4
View File
@@ -82,3 +82,7 @@ class FollowQuery {
}
export default FollowQuery
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T18:05:09.378Z","module_hash":"00de769ca2e93299592c29a5c794911e59b972121763fd907eb568314a559a74","functions":[{"id":"func/FollowQuery.constructor","name":"FollowQuery.constructor","line":17,"end_line":27,"hash":"5389c3c9513d9d576e89969f741c5a511d4fadada98676780ee38b017e2c759a"},{"id":"func/FollowQuery.isFollowing","name":"FollowQuery.isFollowing","line":31,"end_line":41,"hash":"dde01f8501cddd45b091bbf0516994e66b80bd1ca23ffe4b8382c6ee75fbd7ea"},{"id":"func/FollowQuery.listFollowing","name":"FollowQuery.listFollowing","line":44,"end_line":53,"hash":"d354fe051ef6816b948402a94c551792340ec3a4a602aef54aa460a9a119329a"},{"id":"func/FollowQuery.listFollowers","name":"FollowQuery.listFollowers","line":56,"end_line":67,"hash":"332a2bcae7045c3af9e3fbcf5ae5134876be449ada041e7fe1525e2969ae6aee"},{"id":"func/FollowQuery._toHash160","name":"FollowQuery._toHash160","line":69,"end_line":71,"hash":"248ed673be41bc9dcaa0d2fbeed1596784b7bde8877eafe43a05e29e430f9e8b"},{"id":"func/FollowQuery._toCashAddress","name":"FollowQuery._toCashAddress","line":73,"end_line":75,"hash":"0706292919ee44a25e63565558d4d9499188c7c49b3afc39eeb20438f4d5c4d4"},{"id":"func/FollowQuery._nextString","name":"FollowQuery._nextString","line":79,"end_line":81,"hash":"faa7823440ac998c3d5920ebcfe336d7b8df7e705dd6a1177dc8ccbd21f9ecf2"}]}
// mutate4javascript-manifest-end
@@ -27,3 +27,7 @@ class FollowState extends ListUseCase {
}
export default FollowState
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T18:14:50.321Z","module_hash":"605ab43948d281d2c9756032a377369f29aeb9c51cb6df0eaddc106486a0295d","functions":[{"id":"func/FollowState.constructor","name":"FollowState.constructor","line":10,"end_line":12,"hash":"5609d7879b06b8c44554c987d15ff1b1e61fc0adedabc42c7ac3caebb0f71741"},{"id":"func/FollowState.execute","name":"FollowState.execute","line":14,"end_line":26,"hash":"0c610f8b4d676fff9d0dbba025370748a8ad7b1567712f17064322b6e15eade2"}]}
// mutate4javascript-manifest-end
@@ -28,3 +28,7 @@ export class FollowListUseCase extends ListUseCase {
return { [this.addrField]: addr, [this.resultField]: list }
}
}
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T18:16:27.234Z","module_hash":"1424c15ec8acd2179412ef5b347107746ef1e035a47fc1fac595e96c194526f8","functions":[{"id":"func/FollowListUseCase.constructor","name":"FollowListUseCase.constructor","line":15,"end_line":20,"hash":"f5f3b2832eb47baeb416b6b36122163d7479444e82f471a7d9b34f800639208c"},{"id":"func/FollowListUseCase.execute","name":"FollowListUseCase.execute","line":22,"end_line":29,"hash":"3d9258e7bb03192db5b56eac38819604dfdc43ee63b47cc6445754d36ab1848f"}]}
// mutate4javascript-manifest-end
@@ -18,3 +18,7 @@ class ListFollowers extends FollowListUseCase {
}
export default ListFollowers
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T18:12:50.678Z","module_hash":"df81cf75d36c396a429313ca0b95620e66c923146a4f3a987239704d233a2609","functions":[{"id":"func/ListFollowers.constructor","name":"ListFollowers.constructor","line":10,"end_line":17,"hash":"63a34c60f43c67273227749b2cf5b8353d3309b9eaf7287ca95ddffb8613bcf6"}]}
// mutate4javascript-manifest-end
@@ -18,3 +18,7 @@ class ListFollowing extends FollowListUseCase {
}
export default ListFollowing
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T18:12:16.594Z","module_hash":"a99d64bc3dd1ae97bdb2e9b39185b696f5af42ee48630df8c8330c82a6ac208d","functions":[{"id":"func/ListFollowing.constructor","name":"ListFollowing.constructor","line":10,"end_line":17,"hash":"86dd5244ce7fc655582e51b865f172ee95448838f419b797d69ca442786f71fc"}]}
// mutate4javascript-manifest-end
@@ -100,6 +100,14 @@ describe('#FollowQuery', () => {
assert.sameMembers(result, [FOLLOWER, OTHER_FOLLOWER])
})
it('_nextString returns the lexicographic exclusive upper bound of a prefix', () => {
const query = new FollowQuery({ followsDb: makeFollowsDb({}) })
// The upper bound must be exactly the prefix with its last character
// incremented, so a [gte, lt) scan captures precisely the prefix's records.
assert.equal(query._nextString(`${FOLLOWER}:`), `${FOLLOWER};`)
assert.equal(query._nextString('abc'), 'abd')
})
it('listFollowers ignores unfollow records', async () => {
const hash160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9'
const followsDb = makeFollowsDb({
@@ -69,4 +69,34 @@ describe('#FollowState', () => {
assert.match(err.message, /followeeAddr is required/)
}
})
it('should reject an empty-string followerAddr', async () => {
const useCase = new FollowState({ adapters: makeAdapters(false) })
try {
await useCase.execute({ followerAddr: '', followeeAddr: 'bitcoincash:followee' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /followerAddr is required/)
}
})
it('should reject an empty-string followeeAddr', async () => {
const useCase = new FollowState({ adapters: makeAdapters(false) })
try {
await useCase.execute({ followerAddr: 'bitcoincash:follower', followeeAddr: '' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /followeeAddr is required/)
}
})
it('should reject a non-string followerAddr', async () => {
const useCase = new FollowState({ adapters: makeAdapters(false) })
try {
await useCase.execute({ followerAddr: 42, followeeAddr: 'bitcoincash:followee' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /followerAddr is required/)
}
})
})
@@ -42,4 +42,24 @@ describe('#ListFollowers', () => {
assert.match(err.message, /followeeAddr is required/)
}
})
it('should reject an empty-string followeeAddr', async () => {
const useCase = new ListFollowers({ adapters: makeAdapters([]) })
try {
await useCase.execute({ followeeAddr: '' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /followeeAddr is required/)
}
})
it('should reject a non-string followeeAddr', async () => {
const useCase = new ListFollowers({ adapters: makeAdapters([]) })
try {
await useCase.execute({ followeeAddr: 42 })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /followeeAddr is required/)
}
})
})
@@ -42,4 +42,24 @@ describe('#ListFollowing', () => {
assert.match(err.message, /followerAddr is required/)
}
})
it('should reject an empty-string followerAddr', async () => {
const useCase = new ListFollowing({ adapters: makeAdapters([]) })
try {
await useCase.execute({ followerAddr: '' })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /followerAddr is required/)
}
})
it('should reject a non-string followerAddr', async () => {
const useCase = new ListFollowing({ adapters: makeAdapters([]) })
try {
await useCase.execute({ followerAddr: 42 })
assert.fail('expected error')
} catch (err) {
assert.match(err.message, /followerAddr is required/)
}
})
})