Paper trading system with 5 commands (trade_topup, trade_buy,
trade_sell, trade_convert, trade_stats). Supports VN stocks via TCBS,
crypto via CoinGecko, forex via ER-API, and gold via PAX Gold proxy.
Per-user portfolio stored in KV with 60s price caching. 54 new tests.
Unit tests for all trading module files. Use existing test infrastructure: vitest, makeFakeKv, fake bot helpers.
Test files
tests/modules/trading/symbols.test.js
Test
Assertion
SYMBOLS has 9 entries
Object.keys(SYMBOLS).length === 9
Every entry has category, apiId, label
Shape check
getSymbol case-insensitive
getSymbol("btc") === getSymbol("BTC")
getSymbol unknown returns undefined
getSymbol("NOPE") === undefined
getSymbol falsy input returns undefined
getSymbol(""), getSymbol(null)
listSymbols groups by category
Contains "crypto", "stock", "others" headers
CURRENCIES has VND and USD
Set membership
tests/modules/trading/format.test.js
Test
Assertion
formatVND(15000000)
"15.000.000 VND"
formatVND(0)
"0 VND"
formatVND(500)
"500 VND"
formatUSD(1234.5)
"$1,234.50"
formatUSD(0)
"$0.00"
formatCrypto(0.001)
"0.001"
formatCrypto(1.00000000)
"1"
formatCrypto(0.12345678)
"0.12345678"
formatStock(1.7)
"1"
formatStock(100)
"100"
formatAmount dispatches correctly
BTC->crypto, TCB->stock, GOLD->others
formatCurrency dispatches
VND->formatVND, USD->formatUSD
tests/modules/trading/portfolio.test.js
Test
Assertion
emptyPortfolio has correct shape
All keys present, zeroed
getPortfolio returns empty for new user
Uses fake KV
getPortfolio returns stored data
Pre-seed KV
addCurrency increases balance
addCurrency(p, "VND", 1000)
deductCurrency succeeds
Sufficient balance
deductCurrency fails insufficient
Returns { ok: false, balance }
deductCurrency exact balance
Returns { ok: true }, balance = 0
addAsset correct category
BTC -> crypto, TCB -> stock
deductAsset succeeds
Sufficient holding
deductAsset fails insufficient
Returns { ok: false, held }
deductAsset to zero removes key
Key deleted from category
savePortfolio round-trips
Write then read
tests/modules/trading/prices.test.js
Strategy: Mock fetch globally in vitest to return canned API responses. Do NOT call real APIs.
Test
Assertion
fetchPrices merges all 3 sources
Correct shape with all categories
getPrices returns cache when fresh
Only 1 fetch call if called twice within 60s
getPrices refetches when stale
Simulated stale timestamp
getPrice returns correct value
getPrice(db, "BTC") returns mocked VND price
getForexRate VND returns 1
No fetch needed
getForexRate USD returns rate
From mocked forex response
Partial API failure
One API rejects; others still returned
All APIs fail, stale cache < 5min
Returns stale cache
All APIs fail, no cache
Throws with user-friendly message
tests/modules/trading/commands.test.js
Strategy: Integration-style tests. Use makeFakeKv for real KV behavior. Mock fetch for price APIs. Simulate grammY ctx with ctx.match and ctx.reply spy.