Files
psf-memo/test/unit/memo-reply.test.js

116 lines
3.8 KiB
JavaScript

/*
Unit tests for the Memo reply behavior slice (src/services/memo-reply.js).
These tests express the observable behavior described by
specs/reply-memo.feature:
- a valid reply broadcasts an OP_RETURN transaction carrying the Memo reply
prefix (0x6d03), the parent txid bytes, and the message text; the thread
reflects the new reply.
- an empty reply is rejected with a validation error and nothing is broadcast.
- an over-long reply is rejected with a length error and nothing is broadcast.
*/
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const MemoReply = require('../../src/services/memo-reply')
const { fakeWallet } = require('../helpers/fake-wallet')
const { registerMemoActionTests } = require('./memo-action-helpers')
const PARENT_TXID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
// A fake thread that records replies added to the thread view.
function fakeThread (rootTxid = PARENT_TXID) {
const replies = []
return {
rootTxid,
replies,
addReply: (r) => replies.push(r)
}
}
// Decode the reply text from a raw Uint8Array payload (skipping the 32-byte parent txid).
function decodeReplyText (raw) {
const decoder = new TextDecoder()
return decoder.decode(raw.slice(32))
}
// Decode the parent txid from a raw Uint8Array payload.
function decodeParentTxid (raw) {
return Buffer.from(raw.slice(0, 32)).toString('hex')
}
registerMemoActionTests({
Action: MemoReply,
method: 'reply',
MAX: 184,
lengthCode: 'reply_length',
validationCode: 'reply_validation',
label: 'a reply',
storeKey: 'thread',
storeFactory: fakeThread,
assertStoreEmpty: (thread) => assert.equal(thread.replies.length, 0),
assertBroadcastMsg: (broadcast, value) => assert.equal(decodeReplyText(broadcast.msg), value),
byteBased: true,
extraArgs: [PARENT_TXID]
})
test('MEMO_REPLY_PREFIX is the Memo reply action 0x6d03', () => {
assert.equal(MemoReply.MEMO_REPLY_PREFIX, '6d03')
})
test('replying with a valid message broadcasts an OP_RETURN with the Memo reply prefix and payload', async () => {
const wallet = fakeWallet()
const thread = fakeThread()
const memoReply = new MemoReply({ wallet, thread })
const txid = await memoReply.reply('hello memo', PARENT_TXID)
assert.equal(txid, 'fake-txid')
assert.equal(wallet.broadcasts.length, 1)
const b = wallet.broadcasts[0]
assert.equal(b.prefix, '6d03')
assert.ok(b.msg instanceof Uint8Array)
assert.equal(decodeParentTxid(b.msg), PARENT_TXID)
assert.equal(decodeReplyText(b.msg), 'hello memo')
// The thread reflects the new reply from this address with this text.
assert.equal(thread.replies.length, 1)
assert.equal(thread.replies[0].text, 'hello memo')
assert.equal(thread.replies[0].address, wallet.walletInfo.cashAddress)
assert.equal(thread.replies[0].parentTxid, PARENT_TXID)
})
test('replying without a wallet reports a missing-wallet error', async () => {
const memoReply = new MemoReply({})
await assert.rejects(
memoReply.reply('hello memo', PARENT_TXID),
(err) => /wallet/i.test(err.message)
)
})
test('replying with an invalid parent txid reports a clear error', async () => {
const wallet = fakeWallet()
const memoReply = new MemoReply({ wallet })
await assert.rejects(
memoReply.reply('hello memo', 'not-a-txid'),
(err) => /txid/i.test(err.message)
)
assert.equal(wallet.broadcasts.length, 0)
})
test('replying with a wrong-length but valid-hex parent txid is rejected', async () => {
const wallet = fakeWallet()
const memoReply = new MemoReply({ wallet })
// 10 hex characters are valid hex but not the required 64-character txid.
await assert.rejects(
memoReply.reply('hello memo', 'a'.repeat(10)),
(err) => /64-character hex/i.test(err.message)
)
assert.equal(wallet.broadcasts.length, 0)
})