mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-22 17:22:01 -07:00
Refactor mute-user: reduce DRY, add property coverage
Extract shared _getState/_getList helpers in the client MemoDb, a shared _loadState/_setState base in ProfilePage, and a runAction helper for the profile follow/mute handlers. Add property tests for the MemoMute service, the DB MuteQuery adapter, and the indexer handleMute action. Preserve behavior; all unit, acceptance, and property suites pass across client, DB, and indexer. By refactorer.
This commit is contained in:
@@ -75,49 +75,21 @@ function Profile (props) {
|
||||
setThreadTxid(null)
|
||||
}
|
||||
|
||||
const handleFollow = async () => {
|
||||
const runAction = async (method, failMsg) => {
|
||||
if (!profilePage || busy) return
|
||||
setBusy(true)
|
||||
try {
|
||||
await profilePage.follow()
|
||||
await profilePage[method]()
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to follow')
|
||||
setError(err.message || failMsg)
|
||||
}
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
const handleUnfollow = async () => {
|
||||
if (!profilePage || busy) return
|
||||
setBusy(true)
|
||||
try {
|
||||
await profilePage.unfollow()
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to unfollow')
|
||||
}
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
const handleMute = async () => {
|
||||
if (!profilePage || busy) return
|
||||
setBusy(true)
|
||||
try {
|
||||
await profilePage.mute()
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to mute')
|
||||
}
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
const handleUnmute = async () => {
|
||||
if (!profilePage || busy) return
|
||||
setBusy(true)
|
||||
try {
|
||||
await profilePage.unmute()
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to unmute')
|
||||
}
|
||||
setBusy(false)
|
||||
}
|
||||
const handleFollow = () => runAction('follow', 'Failed to follow')
|
||||
const handleUnfollow = () => runAction('unfollow', 'Failed to unfollow')
|
||||
const handleMute = () => runAction('mute', 'Failed to mute')
|
||||
const handleUnmute = () => runAction('unmute', 'Failed to unmute')
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
|
||||
@@ -31,51 +31,15 @@ class MemoDb {
|
||||
}
|
||||
|
||||
async getFollowState (followerAddr, followeeAddr) {
|
||||
try {
|
||||
const result = await this.axios.get(
|
||||
`${config.backend}/follow/state`,
|
||||
{
|
||||
params: {
|
||||
follower: followerAddr,
|
||||
followee: followeeAddr
|
||||
}
|
||||
}
|
||||
)
|
||||
return result.data.following === true
|
||||
} catch (err) {
|
||||
console.error('Error in getFollowState()')
|
||||
throw err
|
||||
}
|
||||
return this._getState('/follow/state', 'getFollowState', { follower: followerAddr, followee: followeeAddr }, 'following')
|
||||
}
|
||||
|
||||
async getMuteState (muterAddr, muteeAddr) {
|
||||
try {
|
||||
const result = await this.axios.get(
|
||||
`${config.backend}/mute/state`,
|
||||
{
|
||||
params: {
|
||||
muter: muterAddr,
|
||||
mutee: muteeAddr
|
||||
}
|
||||
}
|
||||
)
|
||||
return result.data.muted === true
|
||||
} catch (err) {
|
||||
console.error('Error in getMuteState()')
|
||||
throw err
|
||||
}
|
||||
return this._getState('/mute/state', 'getMuteState', { muter: muterAddr, mutee: muteeAddr }, 'muted')
|
||||
}
|
||||
|
||||
async getMuted (muterAddr) {
|
||||
try {
|
||||
const result = await this.axios.get(
|
||||
`${config.backend}/mute/muted/${encodeURIComponent(muterAddr)}`
|
||||
)
|
||||
return result.data.muted || []
|
||||
} catch (err) {
|
||||
console.error('Error in getMuted()')
|
||||
throw err
|
||||
}
|
||||
return this._getList(`/mute/muted/${encodeURIComponent(muterAddr)}`, 'getMuted', 'muted')
|
||||
}
|
||||
|
||||
async getTopics () {
|
||||
@@ -87,28 +51,31 @@ class MemoDb {
|
||||
}
|
||||
|
||||
async getTopicFollowState (room, addr) {
|
||||
return this._getState(`/topics/${encodeURIComponent(room)}/follow/state`, 'getTopicFollowState', { addr }, 'following')
|
||||
}
|
||||
|
||||
async getTopicFollowers (room) {
|
||||
return this._getList(`/topics/${encodeURIComponent(room)}/followers`, 'getTopicFollowers', 'followers')
|
||||
}
|
||||
|
||||
// GET a boolean state endpoint and coerce the named field to a boolean.
|
||||
async _getState (path, name, params, field) {
|
||||
try {
|
||||
const result = await this.axios.get(
|
||||
`${config.backend}/topics/${encodeURIComponent(room)}/follow/state`,
|
||||
{
|
||||
params: { addr }
|
||||
}
|
||||
)
|
||||
return result.data.following === true
|
||||
const result = await this.axios.get(`${config.backend}${path}`, { params })
|
||||
return result.data[field] === true
|
||||
} catch (err) {
|
||||
console.error('Error in getTopicFollowState()')
|
||||
console.error(`Error in ${name}()`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getTopicFollowers (room) {
|
||||
// GET a list endpoint and return the named array field, defaulting to [].
|
||||
async _getList (path, name, field) {
|
||||
try {
|
||||
const result = await this.axios.get(
|
||||
`${config.backend}/topics/${encodeURIComponent(room)}/followers`
|
||||
)
|
||||
return result.data.followers || []
|
||||
const result = await this.axios.get(`${config.backend}${path}`)
|
||||
return result.data[field] || []
|
||||
} catch (err) {
|
||||
console.error('Error in getTopicFollowers()')
|
||||
console.error(`Error in ${name}()`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ class ProfilePage {
|
||||
const data = await this.memoDb.getPostsByAddr(this.addr, { limit, offset })
|
||||
this.posts = data.posts || []
|
||||
this.pagination = data.pagination || null
|
||||
this.followState = await this._loadFollowState()
|
||||
this.muteState = await this._loadMuteState()
|
||||
this.followState = await this._loadState('getFollowState')
|
||||
this.muteState = await this._loadState('getMuteState')
|
||||
|
||||
return {
|
||||
posts: this.posts,
|
||||
@@ -54,20 +54,11 @@ class ProfilePage {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the viewer's follow state for the target address, or false when
|
||||
// there is no viewer or the profile is the viewer's own.
|
||||
async _loadFollowState () {
|
||||
// Fetch a viewer state for the target address, or false when there is no
|
||||
// viewer or the profile is the viewer's own.
|
||||
async _loadState (method) {
|
||||
if (this.myAddr && !this.isOwnProfile()) {
|
||||
return this.memoDb.getFollowState(this.myAddr, this.addr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Fetch the viewer's mute state for the target address, or false when
|
||||
// there is no viewer or the profile is the viewer's own.
|
||||
async _loadMuteState () {
|
||||
if (this.myAddr && !this.isOwnProfile()) {
|
||||
return this.memoDb.getMuteState(this.myAddr, this.addr)
|
||||
return this.memoDb[method](this.myAddr, this.addr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -94,12 +85,7 @@ class ProfilePage {
|
||||
|
||||
// Delegate follow/unfollow to the injected handler and reflect the new state.
|
||||
async _setFollowState (method, nextState) {
|
||||
if (!this.memoFollow) {
|
||||
throw new Error('Profile page requires a memo follow handler.')
|
||||
}
|
||||
await this.memoFollow[method](this.addr)
|
||||
this.followState = nextState
|
||||
return { ok: true }
|
||||
return this._setState(this.memoFollow, 'follow', 'followState', method, nextState)
|
||||
}
|
||||
|
||||
canMute () {
|
||||
@@ -120,11 +106,17 @@ class ProfilePage {
|
||||
|
||||
// Delegate mute/unmute to the injected handler and reflect the new state.
|
||||
async _setMuteState (method, nextState) {
|
||||
if (!this.memoMute) {
|
||||
throw new Error('Profile page requires a memo mute handler.')
|
||||
return this._setState(this.memoMute, 'mute', 'muteState', method, nextState)
|
||||
}
|
||||
|
||||
// Delegate a follow/mute action to the injected handler and reflect the new
|
||||
// state on the matching field.
|
||||
async _setState (handler, label, stateField, method, nextState) {
|
||||
if (!handler) {
|
||||
throw new Error(`Profile page requires a memo ${label} handler.`)
|
||||
}
|
||||
await this.memoMute[method](this.addr)
|
||||
this.muteState = nextState
|
||||
await handler[method](this.addr)
|
||||
this[stateField] = nextState
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
Property tests for the Memo mute/unmute behavior.
|
||||
|
||||
The unit tests probe the broadcast at a couple of fixed addresses. These
|
||||
properties cover broad input ranges so the invariants hold everywhere:
|
||||
|
||||
- conservation: mute() and unmute() each broadcast exactly one Memo
|
||||
action carrying the mutee's 20-byte hash160 payload.
|
||||
- hash length: the broadcast payload is always exactly PK_HASH_LENGTH
|
||||
bytes (a hash160).
|
||||
- round trip: mute then unmute toggles the reflected mute state back to
|
||||
false, and vice versa.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const crypto = require('node:crypto')
|
||||
const { seededRandom, forAll, intGen } = require('./harness')
|
||||
const MemoMute = require('../../src/services/memo-mute')
|
||||
|
||||
const rng = seededRandom(20260830)
|
||||
|
||||
const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
|
||||
const CHARS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
||||
|
||||
// Deterministic 20-byte hash160 hex for any input string, mirroring what a
|
||||
// real wallet's bch-js produces for a valid cash address.
|
||||
function hash20 (s) {
|
||||
return crypto.createHash('sha256').update(s).digest('hex').slice(0, 40)
|
||||
}
|
||||
|
||||
// Random cash-address-shaped string (never empty so it passes validation).
|
||||
function addressGen () {
|
||||
const len = intGen(rng, 40, 60)()
|
||||
let out = 'bitcoincash:q'
|
||||
for (let i = 0; i < len; i++) {
|
||||
out += CHARS[Math.floor(rng() * CHARS.length)]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function makeBchjs () {
|
||||
return {
|
||||
Address: {
|
||||
toHash160 (addr) {
|
||||
return hash20(addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeWallet (address = MY_ADDRESS) {
|
||||
return {
|
||||
walletInfo: { cashAddress: address },
|
||||
bchjs: makeBchjs(),
|
||||
broadcasts: [],
|
||||
async getUtxos () {
|
||||
return []
|
||||
},
|
||||
async sendOpReturn (msg, prefix) {
|
||||
this.broadcasts.push({ msg, prefix })
|
||||
return 'aa'.repeat(32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeProfiles () {
|
||||
const state = {}
|
||||
return {
|
||||
setMuteState: (selfAddr, targetAddr, isMuting) => {
|
||||
if (!state[selfAddr]) state[selfAddr] = {}
|
||||
state[selfAddr][targetAddr] = isMuting
|
||||
},
|
||||
getMuteState: (selfAddr, targetAddr) => state[selfAddr]?.[targetAddr] || false
|
||||
}
|
||||
}
|
||||
|
||||
test('mute broadcasts exactly one hash160 payload with the mute prefix', async () => {
|
||||
await forAll(
|
||||
(i) => addressGen(),
|
||||
async (addr) => {
|
||||
const wallet = makeWallet()
|
||||
const memoMute = new MemoMute({ wallet })
|
||||
await memoMute.mute(addr)
|
||||
|
||||
return wallet.broadcasts.length === 1 &&
|
||||
wallet.broadcasts[0].prefix === MemoMute.MEMO_MUTE_PREFIX &&
|
||||
Buffer.isBuffer(wallet.broadcasts[0].msg) &&
|
||||
wallet.broadcasts[0].msg.length === MemoMute.PK_HASH_LENGTH &&
|
||||
wallet.broadcasts[0].msg.toString('hex') === hash20(addr)
|
||||
},
|
||||
{ label: 'mute broadcast conservation and hash160 length' }
|
||||
)
|
||||
})
|
||||
|
||||
test('unmute broadcasts exactly one hash160 payload with the unmute prefix', async () => {
|
||||
await forAll(
|
||||
(i) => addressGen(),
|
||||
async (addr) => {
|
||||
const wallet = makeWallet()
|
||||
const memoMute = new MemoMute({ wallet })
|
||||
await memoMute.unmute(addr)
|
||||
|
||||
return wallet.broadcasts.length === 1 &&
|
||||
wallet.broadcasts[0].prefix === MemoMute.MEMO_UNMUTE_PREFIX &&
|
||||
Buffer.isBuffer(wallet.broadcasts[0].msg) &&
|
||||
wallet.broadcasts[0].msg.length === MemoMute.PK_HASH_LENGTH &&
|
||||
wallet.broadcasts[0].msg.toString('hex') === hash20(addr)
|
||||
},
|
||||
{ label: 'unmute broadcast conservation and hash160 length' }
|
||||
)
|
||||
})
|
||||
|
||||
test('mute then unmute round-trips the reflected mute state', async () => {
|
||||
await forAll(
|
||||
(i) => addressGen(),
|
||||
async (addr) => {
|
||||
const wallet = makeWallet()
|
||||
const profiles = makeProfiles()
|
||||
const memoMute = new MemoMute({ wallet, profiles })
|
||||
|
||||
await memoMute.mute(addr)
|
||||
const afterMute = profiles.getMuteState(MY_ADDRESS, addr)
|
||||
await memoMute.unmute(addr)
|
||||
const afterUnmute = profiles.getMuteState(MY_ADDRESS, addr)
|
||||
|
||||
return afterMute === true && afterUnmute === false
|
||||
},
|
||||
{ label: 'mute/unmute reflected state round trip' }
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Property tests for the MuteQuery adapter.
|
||||
|
||||
The unit tests probe isMuted/listMuted at a few fixed fixtures. These
|
||||
properties cover broad random record sets so the invariants hold everywhere:
|
||||
|
||||
- round trip: a hash160 converts to a cash address and back to the same
|
||||
hash160.
|
||||
- isMuted conservation: an active mute record reports true and an unmute
|
||||
record reports false, regardless of surrounding records.
|
||||
- list consistency: listMuted returns exactly the active mutees for a
|
||||
muter.
|
||||
*/
|
||||
|
||||
import test from 'node:test'
|
||||
import BCHJS from '@psf/bch-js'
|
||||
import { seededRandom, forAll, intGen } from './harness.js'
|
||||
import MuteQuery from '../../src/adapters/mute-query.js'
|
||||
|
||||
const rng = seededRandom(20260830)
|
||||
const bchjs = new BCHJS({ restURL: 'https://api.fullstack.cash/v5/' })
|
||||
|
||||
const HEX = '0123456789abcdef'
|
||||
|
||||
function hash160Gen () {
|
||||
let out = ''
|
||||
for (let i = 0; i < 40; i++) {
|
||||
out += HEX[Math.floor(rng() * HEX.length)]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// An in-memory mutes Db mirroring the LevelDB contract MuteQuery relies on.
|
||||
function makeMutesDb (records) {
|
||||
const store = new Map(records.map((r) => [r.key, r]))
|
||||
return {
|
||||
async get (key) {
|
||||
if (!store.has(key)) {
|
||||
const err = new Error('not found')
|
||||
err.notFound = true
|
||||
throw err
|
||||
}
|
||||
return store.get(key)
|
||||
},
|
||||
iterator (opts = {}) {
|
||||
const entries = Array.from(store.entries()).sort((a, b) => a[0].localeCompare(b[0]))
|
||||
const { gte, lt } = opts
|
||||
const filtered = entries.filter(([key]) => {
|
||||
if (gte && key < gte) return false
|
||||
if (lt && key >= lt) return false
|
||||
return true
|
||||
})
|
||||
let i = 0
|
||||
return {
|
||||
[Symbol.asyncIterator] () {
|
||||
return this
|
||||
},
|
||||
async next () {
|
||||
if (i >= filtered.length) return { value: undefined, done: true }
|
||||
const entry = filtered[i++]
|
||||
return { value: entry, done: false }
|
||||
},
|
||||
async close () {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build a random set of mute records: a mix of muters, hash160s, and
|
||||
// mute/unmute flags. Not every pair is recorded.
|
||||
function recordSetGen () {
|
||||
const muters = []
|
||||
for (let i = 0, n = intGen(rng, 1, 6)(); i < n; i++) muters.push(`bitcoincash:q${hash160Gen()}`)
|
||||
const hash160s = []
|
||||
for (let i = 0, n = intGen(rng, 1, 6)(); i < n; i++) hash160s.push(hash160Gen())
|
||||
|
||||
const records = []
|
||||
for (const muter of muters) {
|
||||
for (const hash160 of hash160s) {
|
||||
if (rng() < 0.4) continue
|
||||
records.push({ key: `${muter}:${hash160}`, unmute: rng() < 0.5 })
|
||||
}
|
||||
}
|
||||
return { muters, hash160s, records }
|
||||
}
|
||||
|
||||
test('hash160 to cash address and back round-trips to the same hash160', async () => {
|
||||
await forAll(
|
||||
(i) => hash160Gen(),
|
||||
(hash160) => {
|
||||
const cash = bchjs.Address.hash160ToCash(hash160)
|
||||
return bchjs.Address.toHash160(cash) === hash160
|
||||
},
|
||||
{ label: 'hash160 <-> cash address round trip' }
|
||||
)
|
||||
})
|
||||
|
||||
test('isMuted is true exactly for active mute records', async () => {
|
||||
await forAll(
|
||||
(i) => recordSetGen(),
|
||||
async ({ records }) => {
|
||||
const query = new MuteQuery({ mutesDb: makeMutesDb(records), bchjs })
|
||||
for (const record of records) {
|
||||
// Muter cash addresses contain a colon ('bitcoincash:q...'), so the
|
||||
// record key has two colons; the hash160 is the trailing segment.
|
||||
const sep = record.key.lastIndexOf(':')
|
||||
const muter = record.key.slice(0, sep)
|
||||
const hash160 = record.key.slice(sep + 1)
|
||||
const mutee = bchjs.Address.hash160ToCash(hash160)
|
||||
const muted = await query.isMuted(muter, mutee)
|
||||
if (muted !== (record.unmute !== true)) return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
{ label: 'isMuted conservation' }
|
||||
)
|
||||
})
|
||||
|
||||
test('listMuted returns exactly the active mutees for a muter', async () => {
|
||||
await forAll(
|
||||
(i) => recordSetGen(),
|
||||
async ({ muters, records }) => {
|
||||
const query = new MuteQuery({ mutesDb: makeMutesDb(records), bchjs })
|
||||
for (const muter of muters) {
|
||||
const prefix = `${muter}:`
|
||||
const expected = records
|
||||
.filter((r) => r.key.startsWith(prefix) && r.unmute !== true)
|
||||
.map((r) => bchjs.Address.hash160ToCash(r.key.slice(prefix.length)))
|
||||
const got = (await query.listMuted(muter)).sort()
|
||||
const want = Array.from(new Set(expected)).sort()
|
||||
if (JSON.stringify(got) !== JSON.stringify(want)) return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
{ label: 'listMuted consistency' }
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
Property tests for the indexer's mute action handler.
|
||||
|
||||
These pin down invariants the unit tests only probe at fixed fixtures:
|
||||
|
||||
- handleMute stores a record keyed `${signerAddr}:${muteePkHash}` with the
|
||||
exact mutee hash160, the correct unmute flag for the prefix, and the
|
||||
surrounding tx context, for any valid mute/unmute payload.
|
||||
- handleMute rejects a wrong-size mutee hash without storing a record and
|
||||
logs a process error instead.
|
||||
*/
|
||||
|
||||
import test from 'node:test'
|
||||
|
||||
import { seededRandom, forAll } from './harness.js'
|
||||
import { handleMute } from '../../src/use-cases/action-types/mute.js'
|
||||
import { PREFIX_MUTE, PREFIX_UNMUTE, PK_HASH_LENGTH } from '../../src/lib/memo-codes.js'
|
||||
|
||||
const rng = seededRandom(20260829)
|
||||
|
||||
function randomHex (len) {
|
||||
const hex = '0123456789abcdef'
|
||||
let out = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
out += hex[Math.floor(rng() * 16)]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function makeDb () {
|
||||
const store = new Map()
|
||||
return {
|
||||
async get (key) {
|
||||
if (!store.has(key)) {
|
||||
const err = new Error('not found')
|
||||
err.notFound = true
|
||||
throw err
|
||||
}
|
||||
return store.get(key)
|
||||
},
|
||||
async create (key, data) {
|
||||
store.set(key, data)
|
||||
return { success: true }
|
||||
},
|
||||
entries () {
|
||||
return Array.from(store.entries())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('handleMute stores the exact mutee hash, unmute flag, and tx context', async () => {
|
||||
await forAll(
|
||||
() => ({
|
||||
unmute: rng() < 0.5,
|
||||
hashHex: randomHex(PK_HASH_LENGTH * 2),
|
||||
signer: `bitcoincash:q${randomHex(40)}`,
|
||||
txid: randomHex(64),
|
||||
seen: Math.floor(rng() * 1e9),
|
||||
blockHeight: Math.floor(rng() * 1e6)
|
||||
}),
|
||||
async ({ unmute, hashHex, signer, txid, seen, blockHeight }) => {
|
||||
const adapters = {
|
||||
muteDb: makeDb(),
|
||||
processErrorDb: makeDb()
|
||||
}
|
||||
const prefix = unmute ? PREFIX_UNMUTE : PREFIX_MUTE
|
||||
|
||||
await handleMute({
|
||||
adapters,
|
||||
txid,
|
||||
signerAddr: signer,
|
||||
seen,
|
||||
blockHeight,
|
||||
decoded: {
|
||||
action: unmute ? 'unmute' : 'mute',
|
||||
prefix,
|
||||
pushDatas: [prefix, Buffer.from(hashHex, 'hex')]
|
||||
}
|
||||
})
|
||||
|
||||
const record = await adapters.muteDb.get(`${signer}:${hashHex}`)
|
||||
return record.muterAddr === signer &&
|
||||
record.muteePkHash === hashHex &&
|
||||
record.unmute === unmute &&
|
||||
record.txid === txid &&
|
||||
record.seen === seen &&
|
||||
record.blockHeight === blockHeight &&
|
||||
adapters.processErrorDb.entries().length === 0
|
||||
},
|
||||
{ label: 'handleMute record invariants' }
|
||||
)
|
||||
})
|
||||
|
||||
test('handleMute rejects a wrong-size mutee hash without storing', async () => {
|
||||
await forAll(
|
||||
() => ({
|
||||
badLen: 1 + Math.floor(rng() * 40)
|
||||
}),
|
||||
async ({ badLen }) => {
|
||||
if (badLen === PK_HASH_LENGTH) return true
|
||||
const adapters = {
|
||||
muteDb: makeDb(),
|
||||
processErrorDb: makeDb()
|
||||
}
|
||||
|
||||
await handleMute({
|
||||
adapters,
|
||||
txid: 'txid-1',
|
||||
signerAddr: 'bitcoincash:qaddr-a',
|
||||
seen: 1,
|
||||
blockHeight: 100,
|
||||
decoded: {
|
||||
action: 'mute',
|
||||
prefix: PREFIX_MUTE,
|
||||
pushDatas: [PREFIX_MUTE, Buffer.alloc(badLen, 1)]
|
||||
}
|
||||
})
|
||||
|
||||
let stored = false
|
||||
try {
|
||||
await adapters.muteDb.get('bitcoincash:qaddr-a:')
|
||||
stored = true
|
||||
} catch (err) {
|
||||
// expected: not stored
|
||||
}
|
||||
return !stored && adapters.processErrorDb.entries().length > 0
|
||||
},
|
||||
{ label: 'wrong-size mutee hash rejection' }
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user