mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Refresh the mutate4javascript and Gherkin acceptance-mutation manifests, kill the memo-like/memo-reply boundary survivors, and extract the shared FakeDb/makeLevel double used by the txid repair unit and property tests. By architect.
235 lines
6.8 KiB
JavaScript
235 lines
6.8 KiB
JavaScript
/*
|
|
Unit tests for the Memo like wire encoding.
|
|
|
|
A like action embeds the liked post's transaction txid as 32 raw bytes in
|
|
little-endian wire order: the reverse of the 64-character display txid. The
|
|
indexer reverses those bytes back into display order, so the client must
|
|
reverse before embedding.
|
|
*/
|
|
|
|
'use strict'
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const MemoLike = require('../../src/services/memo-like')
|
|
|
|
const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d'
|
|
const POST_TXID = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
|
const WIRE_HEX = 'efcdab8967452301efcdab8967452301efcdab8967452301efcdab8967452301'
|
|
|
|
function makeWallet () {
|
|
return {
|
|
walletInfo: { cashAddress: MY_ADDRESS },
|
|
utxos: [{ txid: 'utxo', value: 100000 }],
|
|
broadcasts: [],
|
|
async getUtxos () {
|
|
return this.utxos
|
|
},
|
|
async sendOpReturn (msg, prefix, bchOutput = []) {
|
|
this.broadcasts.push({ msg, prefix, bchOutput })
|
|
return 'aa'.repeat(32)
|
|
}
|
|
}
|
|
}
|
|
|
|
test('like broadcasts the post txid in little-endian wire order', async () => {
|
|
const wallet = makeWallet()
|
|
const memoLike = new MemoLike({ wallet })
|
|
|
|
await memoLike.like(POST_TXID)
|
|
|
|
assert.equal(wallet.broadcasts.length, 1)
|
|
assert.equal(wallet.broadcasts[0].prefix, MemoLike.MEMO_LIKE_PREFIX)
|
|
const raw = wallet.broadcasts[0].msg
|
|
assert.equal(Buffer.from(raw).toString('hex'), WIRE_HEX)
|
|
})
|
|
|
|
test('like reflects the post txid in display order on the feed store', async () => {
|
|
const wallet = makeWallet()
|
|
const added = []
|
|
const feed = {
|
|
addLike (like) {
|
|
added.push(like)
|
|
},
|
|
posts: []
|
|
}
|
|
const memoLike = new MemoLike({ wallet, feed })
|
|
|
|
await memoLike.like(POST_TXID)
|
|
|
|
assert.equal(added.length, 1)
|
|
assert.equal(added[0].postTxid, POST_TXID)
|
|
})
|
|
|
|
test('like rejects a caller without a wallet', async () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
await assert.rejects(() => memoLike.like(POST_TXID), /requires a wallet/)
|
|
})
|
|
|
|
test('like rejects an invalid post txid before broadcasting', async () => {
|
|
const wallet = makeWallet()
|
|
const memoLike = new MemoLike({ wallet })
|
|
|
|
await assert.rejects(
|
|
() => memoLike.like('not-a-txid'),
|
|
(err) => err.code === 'like_validation'
|
|
)
|
|
assert.equal(wallet.broadcasts.length, 0)
|
|
})
|
|
|
|
test('like rejects a balance below the dust limit', async () => {
|
|
const wallet = makeWallet()
|
|
wallet.utxos = [{ txid: 'utxo', value: 100 }]
|
|
const memoLike = new MemoLike({ wallet })
|
|
|
|
await assert.rejects(
|
|
() => memoLike.like(POST_TXID),
|
|
(err) => err.code === 'like_empty_balance'
|
|
)
|
|
})
|
|
|
|
test('like rejects a tip that exceeds the spendable balance', async () => {
|
|
const wallet = makeWallet()
|
|
wallet.utxos = [{ txid: 'utxo', value: 3000 }]
|
|
const memoLike = new MemoLike({ wallet })
|
|
|
|
await assert.rejects(
|
|
() => memoLike.like(POST_TXID, 4000, MY_ADDRESS),
|
|
(err) => err.code === 'like_balance'
|
|
)
|
|
})
|
|
|
|
test('like requires an author address when a tip is present', async () => {
|
|
const wallet = makeWallet()
|
|
const memoLike = new MemoLike({ wallet })
|
|
|
|
await assert.rejects(
|
|
() => memoLike.like(POST_TXID, 1000),
|
|
(err) => err.code === 'like_validation'
|
|
)
|
|
})
|
|
|
|
test('_validateTipAmount rejects a non-integer or negative tip', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.throws(() => memoLike._validateTipAmount(1.5), (err) => err.code === 'like_validation')
|
|
assert.throws(() => memoLike._validateTipAmount(-1), (err) => err.code === 'like_validation')
|
|
})
|
|
|
|
test('_validateTipAmount rejects a tip below the dust limit', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.throws(() => memoLike._validateTipAmount(1), (err) => err.code === 'like_dust')
|
|
})
|
|
|
|
test('_validateTipAmount rejects a tip above the maximum', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.throws(
|
|
() => memoLike._validateTipAmount(MemoLike.MAX_TIP_SATS + 1),
|
|
(err) => err.code === 'like_maximum'
|
|
)
|
|
})
|
|
|
|
test('_validateTipAmount accepts zero and a valid tip', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.equal(memoLike._validateTipAmount(0), undefined)
|
|
assert.equal(memoLike._validateTipAmount(1000), undefined)
|
|
})
|
|
|
|
test('getSpendableSats sums the utxoStore bchUtxos shape', () => {
|
|
const wallet = {
|
|
utxos: { utxoStore: { bchUtxos: [{ satoshis: 1000 }, { amount: 500 }] } }
|
|
}
|
|
const memoLike = new MemoLike({ wallet })
|
|
|
|
assert.equal(memoLike.getSpendableSats(), 1500)
|
|
})
|
|
|
|
test('getSpendableSats is zero without a wallet', () => {
|
|
assert.equal(new MemoLike().getSpendableSats(), 0)
|
|
})
|
|
|
|
test('like includes a tip output and reflects the tip on the feed', async () => {
|
|
const wallet = makeWallet()
|
|
const added = []
|
|
const feed = {
|
|
addLike (like) {
|
|
added.push(like)
|
|
},
|
|
posts: [{ txid: POST_TXID, likeCount: 2 }]
|
|
}
|
|
const memoLike = new MemoLike({ wallet, feed })
|
|
|
|
await memoLike.like(POST_TXID, 1000, MY_ADDRESS)
|
|
|
|
assert.deepEqual(wallet.broadcasts[0].bchOutput, [{ address: MY_ADDRESS, amountSat: 1000 }])
|
|
assert.equal(added[0].tipSats, 1000)
|
|
assert.equal(feed.posts[0].likeCount, 3)
|
|
})
|
|
|
|
test('_buildTipOutput omits the output for a zero tip', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.deepEqual(memoLike._buildTipOutput(0, MY_ADDRESS), [])
|
|
})
|
|
|
|
test('validate reports ok for a valid post txid', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.deepEqual(memoLike.validate(POST_TXID), { ok: true })
|
|
})
|
|
|
|
test('validateTip reports ok when the tip exactly matches the spendable balance', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.deepEqual(memoLike.validateTip(3000, 3000), { ok: true })
|
|
})
|
|
|
|
test('_validateTipAmount accepts a tip exactly at the maximum', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.equal(memoLike._validateTipAmount(MemoLike.MAX_TIP_SATS), undefined)
|
|
})
|
|
|
|
test('getSpendableSats treats a utxo without a value field as zero', () => {
|
|
const wallet = { utxos: [{ satoshis: 1000 }, { txid: 'no-value' }] }
|
|
const memoLike = new MemoLike({ wallet })
|
|
|
|
assert.equal(memoLike.getSpendableSats(), 1000)
|
|
})
|
|
|
|
test('_requireTipAddress requires an address for a positive tip', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.throws(
|
|
() => memoLike._requireTipAddress(1, ''),
|
|
(err) => err.code === 'like_validation'
|
|
)
|
|
})
|
|
|
|
test('_requireTipAddress accepts a one-character address', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.equal(memoLike._requireTipAddress(1000, 'a'), undefined)
|
|
})
|
|
|
|
test('_buildTipOutput builds the output for a positive tip', () => {
|
|
const memoLike = new MemoLike()
|
|
|
|
assert.deepEqual(memoLike._buildTipOutput(1, MY_ADDRESS), [
|
|
{ address: MY_ADDRESS, amountSat: 1 }
|
|
])
|
|
})
|
|
|
|
test('_incrementPostCount defaults a missing likeCount to zero', () => {
|
|
const feed = { posts: [{ txid: POST_TXID }] }
|
|
const memoLike = new MemoLike({ feed })
|
|
|
|
memoLike._incrementPostCount(POST_TXID)
|
|
|
|
assert.equal(feed.posts[0].likeCount, 1)
|
|
})
|