fiat: e2e tests

This commit is contained in:
pragmaxim
2026-02-26 11:01:29 +01:00
parent 87c20fd4e2
commit da37dfd990
5 changed files with 196 additions and 15 deletions

View File

@@ -46,6 +46,9 @@ var commonTests = map[string]func(t *testing.T, th *TestHandler){
"GetAddress": testGetAddress,
"GetAddressTxids": testGetAddressTxids,
"GetAddressTxs": testGetAddressTxs,
"GetCurrentFiatRates": testGetCurrentFiatRates,
"GetTickersList": testGetTickersList,
"GetMultiTickers": testGetMultiTickers,
}
var utxoOnlyTests = map[string]func(t *testing.T, th *TestHandler){
@@ -106,6 +109,9 @@ type TestHandler struct {
sampleBlockHash string
sampleContractResolved bool
sampleContract string
sampleFiatResolved bool
sampleFiatAvailable bool
sampleFiatTicker fiatTickerResponse
capabilitiesResolved bool
supportsUTXO bool
@@ -120,7 +126,9 @@ type statusEnvelope struct {
}
type statusBlockbook struct {
BestHeight int `json:"bestHeight"`
BestHeight int `json:"bestHeight"`
HasFiatRates bool `json:"hasFiatRates"`
CurrentFiatRatesTime *time.Time `json:"currentFiatRatesTime"`
}
type blockIndexResponse struct {
@@ -180,6 +188,16 @@ type utxoResponse struct {
Height int `json:"height"`
}
type fiatTickerResponse struct {
Timestamp int64 `json:"ts"`
Rates map[string]float32 `json:"rates"`
}
type availableVsCurrenciesResponse struct {
Timestamp int64 `json:"ts"`
Tickers []string `json:"available_currencies"`
}
type wsRequest struct {
ID string `json:"id"`
Method string `json:"method"`

View File

@@ -122,6 +122,81 @@ func testGetAddress(t *testing.T, h *TestHandler) {
}
}
func testGetCurrentFiatRates(t *testing.T, h *TestHandler) {
ticker := h.sampleFiatTickerOrSkip(t)
assertFiatTickerPayload(t, &ticker, "GetCurrentFiatRates")
rate, ok := ticker.Rates["usd"]
if !ok {
t.Fatalf("GetCurrentFiatRates missing requested usd rate")
}
if rate == 0 {
t.Fatalf("GetCurrentFiatRates usd rate must not be zero")
}
}
func testGetTickersList(t *testing.T, h *TestHandler) {
ticker := h.sampleFiatTickerOrSkip(t)
path := fmt.Sprintf("/api/v2/tickers-list?timestamp=%d", ticker.Timestamp)
var list availableVsCurrenciesResponse
h.mustGetFiatJSONOrSkip(t, path, &list)
if list.Timestamp <= 0 {
t.Fatalf("GetTickersList invalid timestamp: %d", list.Timestamp)
}
if len(list.Tickers) == 0 {
t.Fatalf("GetTickersList returned no currencies")
}
for i := range list.Tickers {
assertNonEmptyString(t, list.Tickers[i], "GetTickersList.available_currencies")
}
}
func testGetMultiTickers(t *testing.T, h *TestHandler) {
ticker := h.sampleFiatTickerOrSkip(t)
listPath := fmt.Sprintf("/api/v2/tickers-list?timestamp=%d", ticker.Timestamp)
var list availableVsCurrenciesResponse
h.mustGetFiatJSONOrSkip(t, listPath, &list)
if len(list.Tickers) == 0 {
t.Skipf("Skipping test, no available fiat currencies for timestamp %d", ticker.Timestamp)
}
currency := strings.ToLower(strings.TrimSpace(list.Tickers[0]))
if currency == "" {
t.Fatalf("GetMultiTickers invalid empty currency from tickers-list")
}
var single fiatTickerResponse
singlePath := fmt.Sprintf("/api/v2/tickers?timestamp=%d&currency=%s", ticker.Timestamp, url.QueryEscape(currency))
h.mustGetFiatJSONOrSkip(t, singlePath, &single)
assertFiatTickerPayload(t, &single, "GetMultiTickers.single")
var multi []fiatTickerResponse
multiPath := fmt.Sprintf("/api/v2/multi-tickers?timestamp=%d&currency=%s", ticker.Timestamp, url.QueryEscape(currency))
h.mustGetFiatJSONOrSkip(t, multiPath, &multi)
if len(multi) != 1 {
t.Fatalf("GetMultiTickers expected exactly 1 entry, got %d", len(multi))
}
assertFiatTickerPayload(t, &multi[0], "GetMultiTickers.multi[0]")
if multi[0].Timestamp != single.Timestamp {
t.Fatalf("GetMultiTickers timestamp mismatch: single=%d multi=%d", single.Timestamp, multi[0].Timestamp)
}
singleRate, ok := single.Rates[currency]
if !ok {
t.Fatalf("GetMultiTickers single missing rate for %s", currency)
}
multiRate, ok := multi[0].Rates[currency]
if !ok {
t.Fatalf("GetMultiTickers multi missing rate for %s", currency)
}
if singleRate != multiRate {
t.Fatalf("GetMultiTickers rate mismatch for %s: single=%v multi=%v", currency, singleRate, multiRate)
}
}
func testGetAddressTxids(t *testing.T, h *TestHandler) {
address := h.sampleAddressOrSkip(t)
txid := h.sampleTxIDOrSkip(t)
@@ -203,6 +278,29 @@ func (h *TestHandler) mustGetJSON(t *testing.T, path string, out interface{}) {
}
}
func (h *TestHandler) mustGetFiatJSONOrSkip(t *testing.T, path string, out interface{}) {
t.Helper()
const maxAttempts = 2
for attempt := 1; attempt <= maxAttempts; attempt++ {
status, body := h.getHTTP(t, path)
if status == http.StatusOK {
if err := json.Unmarshal(body, out); err != nil {
t.Fatalf("decode %s: %v", path, err)
}
return
}
if isFiatDataUnavailable(status, body) {
if attempt < maxAttempts {
time.Sleep(time.Duration(attempt) * 300 * time.Millisecond)
continue
}
t.Skipf("Skipping test, fiat data unavailable for %s (HTTP %d: %s)", path, status, preview(body))
}
t.Fatalf("GET %s returned HTTP %d: %s", path, status, preview(body))
}
}
func (h *TestHandler) getHTTP(t *testing.T, path string) (int, []byte) {
t.Helper()
@@ -314,3 +412,11 @@ func isRetryableHTTPStatus(status int) bool {
return false
}
}
func isFiatDataUnavailable(status int, body []byte) bool {
if status != http.StatusBadRequest && status != http.StatusInternalServerError {
return false
}
msg := strings.ToLower(preview(body))
return strings.Contains(msg, "no tickers found") || strings.Contains(msg, "error finding ticker")
}

View File

@@ -436,6 +436,47 @@ func (h *TestHandler) sampleAddressOrSkip(t *testing.T) string {
return address
}
func (h *TestHandler) getSampleFiatTicker(t *testing.T) (fiatTickerResponse, bool) {
if h.sampleFiatResolved {
return h.sampleFiatTicker, h.sampleFiatAvailable
}
h.sampleFiatResolved = true
path := "/api/v2/tickers?currency=usd"
status, body := h.getHTTP(t, path)
if isFiatDataUnavailable(status, body) {
return fiatTickerResponse{}, false
}
if status != http.StatusOK {
t.Fatalf("GET %s returned HTTP %d: %s", path, status, preview(body))
}
var ticker fiatTickerResponse
if err := json.Unmarshal(body, &ticker); err != nil {
t.Fatalf("decode %s: %v", path, err)
}
if ticker.Timestamp <= 0 || len(ticker.Rates) == 0 {
return fiatTickerResponse{}, false
}
h.sampleFiatAvailable = true
h.sampleFiatTicker = ticker
return h.sampleFiatTicker, true
}
func (h *TestHandler) sampleFiatTickerOrSkip(t *testing.T) fiatTickerResponse {
t.Helper()
ticker, found := h.getSampleFiatTicker(t)
if !found {
status := h.getStatus(t)
if !status.HasFiatRates {
t.Skipf("Skipping test, endpoint reports hasFiatRates=false")
}
t.Skipf("Skipping test, fiat ticker data currently unavailable")
}
return ticker
}
func (h *TestHandler) requireCapabilities(t *testing.T, required testCapability, group, test string) bool {
t.Helper()
if required == capabilityNone {

View File

@@ -155,6 +155,22 @@ func assertUTXOListNonNegativeConfirmations(t *testing.T, utxos []utxoResponse,
}
}
func assertFiatTickerPayload(t *testing.T, payload *fiatTickerResponse, context string) {
t.Helper()
if payload.Timestamp <= 0 {
t.Fatalf("%s invalid timestamp: %d", context, payload.Timestamp)
}
if len(payload.Rates) == 0 {
t.Fatalf("%s returned no rates", context)
}
for currency, rate := range payload.Rates {
assertNonEmptyString(t, currency, context+".rates.currency")
if rate == 0 {
t.Fatalf("%s returned zero rate for currency %s", context, currency)
}
}
}
func assertUTXOSetsEqualByOutpoint(t *testing.T, got, want []utxoResponse, context string) {
t.Helper()
gotSet := utxoSetByOutpoint(t, got, context+".got")

View File

@@ -2,14 +2,14 @@
"avalanche": {
"connectivity": ["http", "ws"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"]
},
"bcash": {
"connectivity": ["http"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync",
"EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"],
"sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"]
@@ -30,7 +30,7 @@
"bitcoin": {
"connectivity": ["http"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfo", "WsGetAccountUtxo", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync",
"EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"],
@@ -39,7 +39,7 @@
"bitcoin_testnet": {
"connectivity": ["http"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync",
"EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"],
"sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"]
@@ -47,7 +47,7 @@
"bitcoin_testnet4": {
"connectivity": ["http"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync",
"EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"],
"sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"]
@@ -70,7 +70,7 @@
"bsc": {
"connectivity": ["http", "ws"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"]
},
@@ -119,7 +119,7 @@
"dogecoin": {
"connectivity": ["http"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "MempoolSync"],
"sync": ["ConnectBlocksParallel", "ConnectBlocks"]
},
@@ -165,7 +165,7 @@
"litecoin": {
"connectivity": ["http"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync",
"EstimateSmartFee", "EstimateFee"],
"sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"]
@@ -191,7 +191,7 @@
"zcash": {
"connectivity": ["http"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressTxids", "GetAddressTxs", "GetUtxo", "GetUtxoConfirmedFilter"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync",
"GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"],
"sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"]
@@ -283,35 +283,35 @@
"arbitrum": {
"connectivity": ["http", "ws"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"]
},
"base": {
"connectivity": ["http", "ws"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"]
},
"ethereum": {
"connectivity": ["http", "ws"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"]
},
"optimism": {
"connectivity": ["http", "ws"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"]
},
"polygon": {
"connectivity": ["http", "ws"],
"api": ["Status", "GetBlockIndex", "GetBlockByHeight", "GetBlock", "GetTransaction", "GetTransactionSpecific",
"GetAddress", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"GetAddress", "GetCurrentFiatRates", "GetTickersList", "GetMultiTickers", "GetAddressBasicEVM", "GetAddressTokensEVM", "GetAddressTokenBalances", "GetAddressTxidsPaginationEVM", "GetAddressTxsPaginationEVM", "GetAddressContractFilterEVM", "GetTransactionEVMShape",
"WsGetInfo", "WsGetBlockHash", "WsGetTransaction", "WsGetAccountInfoBasicEVM", "WsGetAccountInfoEVM", "WsGetAccountInfoTxidsConsistencyEVM", "WsGetAccountInfoTxsConsistencyEVM", "WsGetAccountInfoContractFilterEVM", "WsPing"],
"rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"]
}