mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Implement Post a Memo behavior with acceptance pipeline
Implements the post-memo behavior slice per specs/post-memo.feature: - src/services/memo-post.js: compose, validate (empty/length), and broadcast a Memo post OP_RETURN (0x6d02) via the minimal-slp-wallet adapter surface. - Stands up the Babashka APS acceptance pipeline (entrypoint generator, runtime, regex step handlers, convenience runner). - Adds focused unit tests for the memo behavior. By coder.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
Unit tests for the Memo post behavior slice (src/services/memo-post.js).
|
||||
|
||||
These tests express the observable behavior described by
|
||||
specs/post-memo.feature:
|
||||
- a valid memo broadcasts an OP_RETURN transaction carrying the Memo post
|
||||
prefix (0x6d02) and the message text, and the feed reflects the new post.
|
||||
- an empty memo is rejected with a validation error and nothing is broadcast.
|
||||
- an over-long memo is rejected with a length error and nothing is broadcast.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const MemoPost = require('../../src/services/memo-post')
|
||||
|
||||
// A fake wallet that records every broadcast attempt. It satisfies the small
|
||||
// adapter surface the MemoPost module needs: walletInfo, getUtxos(),
|
||||
// sendOpReturn().
|
||||
function fakeWallet (cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d', utxos = [{ txid: 'utxo1' }]) {
|
||||
const broadcasts = []
|
||||
const wallet = {
|
||||
walletInfo: { cashAddress },
|
||||
getUtxos: async () => utxos,
|
||||
sendOpReturn: async (walletInfo, bchUtxos, msg, prefix) => {
|
||||
broadcasts.push({ walletInfo, bchUtxos, msg, prefix })
|
||||
return 'fake-txid'
|
||||
}
|
||||
}
|
||||
wallet.broadcasts = broadcasts
|
||||
return wallet
|
||||
}
|
||||
|
||||
// A fake feed that records posts added to the recent posts feed.
|
||||
function fakeFeed () {
|
||||
const posts = []
|
||||
return { posts, addPost: (p) => posts.push(p) }
|
||||
}
|
||||
|
||||
test('MEMO_POST_PREFIX is the Memo post action 0x6d02', () => {
|
||||
assert.equal(MemoPost.MEMO_POST_PREFIX, '6d02')
|
||||
})
|
||||
|
||||
test('posting a valid memo broadcasts an OP_RETURN with the Memo post prefix and message', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
const memoPost = new MemoPost({ wallet, feed })
|
||||
|
||||
const txid = await memoPost.post('hello memo')
|
||||
|
||||
assert.equal(txid, 'fake-txid')
|
||||
assert.equal(wallet.broadcasts.length, 1)
|
||||
const b = wallet.broadcasts[0]
|
||||
assert.equal(b.prefix, '6d02')
|
||||
assert.equal(b.msg, 'hello memo')
|
||||
// The broadcast uses the wallet's spendable UTXOs.
|
||||
assert.equal(b.bchUtxos.length, 1)
|
||||
// The wallet info passed to sendOpReturn is the authenticated wallet.
|
||||
assert.equal(b.walletInfo.cashAddress, wallet.walletInfo.cashAddress)
|
||||
|
||||
// The feed reflects the new post from this address with this text.
|
||||
assert.equal(feed.posts.length, 1)
|
||||
assert.equal(feed.posts[0].text, 'hello memo')
|
||||
assert.equal(feed.posts[0].address, wallet.walletInfo.cashAddress)
|
||||
})
|
||||
|
||||
test('posting a memo at the maximum length (217) is accepted', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoPost = new MemoPost({ wallet })
|
||||
|
||||
const msg = 'x'.repeat(217)
|
||||
const txid = await memoPost.post(msg)
|
||||
assert.equal(txid, 'fake-txid')
|
||||
assert.equal(wallet.broadcasts[0].msg, msg)
|
||||
})
|
||||
|
||||
test('posting an empty memo throws a validation error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
const memoPost = new MemoPost({ wallet, feed })
|
||||
|
||||
await assert.rejects(
|
||||
memoPost.post(''),
|
||||
(err) => err.code === 'memo_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(feed.posts.length, 0)
|
||||
})
|
||||
|
||||
test('posting a whitespace-only memo throws a validation error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoPost = new MemoPost({ wallet })
|
||||
|
||||
await assert.rejects(
|
||||
memoPost.post(' '),
|
||||
(err) => err.code === 'memo_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
})
|
||||
|
||||
test('posting a non-string memo throws a validation error', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const memoPost = new MemoPost({ wallet })
|
||||
|
||||
await assert.rejects(
|
||||
memoPost.post(42),
|
||||
(err) => err.code === 'memo_validation'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
})
|
||||
|
||||
test('posting an over-long memo (218) throws a length error and broadcasts nothing', async () => {
|
||||
const wallet = fakeWallet()
|
||||
const feed = fakeFeed()
|
||||
const memoPost = new MemoPost({ wallet, feed })
|
||||
|
||||
await assert.rejects(
|
||||
memoPost.post('y'.repeat(218)),
|
||||
(err) => err.code === 'memo_length'
|
||||
)
|
||||
assert.equal(wallet.broadcasts.length, 0)
|
||||
assert.equal(feed.posts.length, 0)
|
||||
})
|
||||
|
||||
test('posting without a wallet reports a missing-wallet error', async () => {
|
||||
const memoPost = new MemoPost({})
|
||||
await assert.rejects(
|
||||
memoPost.post('hello memo'),
|
||||
(err) => /wallet/i.test(err.message)
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user