mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
- Add MemoSetName service with 77-byte UTF-8 validation and broadcast. - Add SetNamePage controller with byte counter and navigation to /account. - Add AccountPage controller showing stored name and Set Name button. - Add Profiles session store to share the new name across pages. - Add React views /memo/set-name and /account, plus nav link. - Update acceptance handlers for set-name scenarios and fix fake wallet sendOpReturn signature to match public minimal-slp-wallet API. - Update existing post memo tests to match the corrected wallet API. By coder.
162 lines
5.3 KiB
JavaScript
162 lines
5.3 KiB
JavaScript
/*
|
|
Unit tests for the Memo set-name behavior slice (src/services/memo-set-name.js).
|
|
|
|
These tests express the observable behavior described by specs/set-name.feature:
|
|
- a valid name broadcasts an OP_RETURN transaction carrying the Memo set-name
|
|
prefix (0x6d01) and the name text, and the profile store reflects the new
|
|
name.
|
|
- an empty name is rejected with a validation error and nothing is broadcast.
|
|
- an over-long name is rejected with a length error and nothing is broadcast.
|
|
*/
|
|
|
|
'use strict'
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const MemoSetName = require('../../src/services/memo-set-name')
|
|
|
|
// A fake wallet that records every broadcast attempt. It satisfies the small
|
|
// adapter surface the MemoSetName module needs: walletInfo, getUtxos(),
|
|
// sendOpReturn().
|
|
function fakeWallet (cashAddress = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d', utxos = [{ txid: 'utxo1' }]) {
|
|
const broadcasts = []
|
|
const wallet = {
|
|
walletInfo: { cashAddress },
|
|
getUtxos: async () => utxos,
|
|
sendOpReturn: async (msg, prefix) => {
|
|
broadcasts.push({ msg, prefix })
|
|
return 'fake-txid'
|
|
}
|
|
}
|
|
wallet.broadcasts = broadcasts
|
|
return wallet
|
|
}
|
|
|
|
// A fake profile store that records names set for addresses.
|
|
function fakeProfiles () {
|
|
const names = {}
|
|
return {
|
|
names,
|
|
setName: (addr, name) => { names[addr] = name },
|
|
getName: (addr) => names[addr] || null
|
|
}
|
|
}
|
|
|
|
test('MEMO_SET_NAME_PREFIX is the Memo set-name action 0x6d01', () => {
|
|
assert.equal(MemoSetName.MEMO_SET_NAME_PREFIX, '6d01')
|
|
})
|
|
|
|
test('setting a valid name broadcasts an OP_RETURN with the Memo set-name prefix and name', async () => {
|
|
const wallet = fakeWallet()
|
|
const profiles = fakeProfiles()
|
|
const memoSetName = new MemoSetName({ wallet, profiles })
|
|
|
|
const txid = await memoSetName.setName('trout')
|
|
|
|
assert.equal(txid, 'fake-txid')
|
|
assert.equal(wallet.broadcasts.length, 1)
|
|
const b = wallet.broadcasts[0]
|
|
assert.equal(b.prefix, '6d01')
|
|
assert.equal(b.msg, 'trout')
|
|
|
|
// The profile store reflects the new name for this address.
|
|
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), 'trout')
|
|
})
|
|
|
|
test('setting a name at the maximum byte length (77) is accepted', async () => {
|
|
const wallet = fakeWallet()
|
|
const memoSetName = new MemoSetName({ wallet })
|
|
|
|
const name = 'x'.repeat(77)
|
|
const txid = await memoSetName.setName(name)
|
|
assert.equal(txid, 'fake-txid')
|
|
assert.equal(wallet.broadcasts[0].msg, name)
|
|
})
|
|
|
|
test('setting a name at the maximum byte length with multi-byte characters is accepted', async () => {
|
|
const wallet = fakeWallet()
|
|
const memoSetName = new MemoSetName({ wallet })
|
|
|
|
// 38 'é' characters are 76 bytes in UTF-8.
|
|
const name = 'é'.repeat(38)
|
|
assert.equal(Buffer.byteLength(name, 'utf8'), 76)
|
|
const txid = await memoSetName.setName(name)
|
|
assert.equal(txid, 'fake-txid')
|
|
})
|
|
|
|
test('setting an over-long name in bytes (78) throws a length error even when char count is lower', async () => {
|
|
const wallet = fakeWallet()
|
|
const memoSetName = new MemoSetName({ wallet })
|
|
|
|
// 40 'é' characters are 80 bytes, exceeding the 77-byte limit.
|
|
const name = 'é'.repeat(40)
|
|
assert.ok(Buffer.byteLength(name, 'utf8') > 77)
|
|
await assert.rejects(
|
|
memoSetName.setName(name),
|
|
(err) => err.code === 'name_length'
|
|
)
|
|
assert.equal(wallet.broadcasts.length, 0)
|
|
})
|
|
|
|
test('setting an empty name throws a validation error and broadcasts nothing', async () => {
|
|
const wallet = fakeWallet()
|
|
const profiles = fakeProfiles()
|
|
const memoSetName = new MemoSetName({ wallet, profiles })
|
|
|
|
await assert.rejects(
|
|
memoSetName.setName(''),
|
|
(err) => err.code === 'name_validation'
|
|
)
|
|
assert.equal(wallet.broadcasts.length, 0)
|
|
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
|
})
|
|
|
|
test('setting a whitespace-only or non-string name throws a validation error and broadcasts nothing', async () => {
|
|
for (const invalid of [' ', 42]) {
|
|
const wallet = fakeWallet()
|
|
const memoSetName = new MemoSetName({ wallet })
|
|
|
|
await assert.rejects(
|
|
memoSetName.setName(invalid),
|
|
(err) => err.code === 'name_validation'
|
|
)
|
|
assert.equal(wallet.broadcasts.length, 0)
|
|
}
|
|
})
|
|
|
|
test('setting an over-long name (78) throws a length error and broadcasts nothing', async () => {
|
|
const wallet = fakeWallet()
|
|
const profiles = fakeProfiles()
|
|
const memoSetName = new MemoSetName({ wallet, profiles })
|
|
|
|
await assert.rejects(
|
|
memoSetName.setName('y'.repeat(78)),
|
|
(err) => err.code === 'name_length'
|
|
)
|
|
assert.equal(wallet.broadcasts.length, 0)
|
|
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
|
})
|
|
|
|
test('setting a name without a wallet reports a missing-wallet error', async () => {
|
|
const memoSetName = new MemoSetName({})
|
|
await assert.rejects(
|
|
memoSetName.setName('trout'),
|
|
(err) => /wallet/i.test(err.message)
|
|
)
|
|
})
|
|
|
|
test('a failed broadcast does not update the profile store', async () => {
|
|
const wallet = fakeWallet()
|
|
wallet.sendOpReturn = async () => { throw new Error('broadcast failure') }
|
|
const profiles = fakeProfiles()
|
|
const memoSetName = new MemoSetName({ wallet, profiles })
|
|
|
|
await assert.rejects(
|
|
memoSetName.setName('trout'),
|
|
(err) => /broadcast failure/i.test(err.message)
|
|
)
|
|
assert.equal(wallet.broadcasts.length, 0)
|
|
assert.equal(profiles.getName(wallet.walletInfo.cashAddress), null)
|
|
})
|