From 87c20fd4e2c95ff7d4df759a56dcafb5b40f9dec Mon Sep 17 00:00:00 2001 From: pragmaxim Date: Thu, 26 Feb 2026 07:00:23 +0100 Subject: [PATCH] hardening e2e tests for newly discovered bugs --- tests/api/evm_tests.go | 4 ++ tests/api/http_tests.go | 34 +++++++++++++--- tests/api/test_helpers.go | 86 ++++++++++++++++++++++++++++++++++++++- tests/api/ws_tests.go | 2 +- 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/tests/api/evm_tests.go b/tests/api/evm_tests.go index 609b1170..e602aeda 100644 --- a/tests/api/evm_tests.go +++ b/tests/api/evm_tests.go @@ -32,6 +32,7 @@ func testGetAddressTxidsPaginationEVM(t *testing.T, h *TestHandler) { assertAddressMatches(t, page1.Address, address, "GetAddressTxidsPaginationEVM.page1.address") assertPageMeta(t, page1.Page, page1.ItemsOnPage, page1.TotalPages, page1.Txs, "GetAddressTxidsPaginationEVM.page1") + assertPageSizeUpperBound(t, len(page1.Txids), page1.ItemsOnPage, evmHistoryPageSize, "GetAddressTxidsPaginationEVM.page1.txids") if len(page1.Txids) == 0 { t.Fatalf("GetAddressTxidsPaginationEVM page 1 returned no txids") } @@ -48,6 +49,7 @@ func testGetAddressTxidsPaginationEVM(t *testing.T, h *TestHandler) { assertAddressMatches(t, page2.Address, address, "GetAddressTxidsPaginationEVM.page2.address") assertPageMeta(t, page2.Page, page2.ItemsOnPage, page2.TotalPages, page2.Txs, "GetAddressTxidsPaginationEVM.page2") + assertPageSizeUpperBound(t, len(page2.Txids), page2.ItemsOnPage, evmHistoryPageSize, "GetAddressTxidsPaginationEVM.page2.txids") if page2.Page != evmHistoryPage+1 { t.Fatalf("GetAddressTxidsPaginationEVM page mismatch: got %d, want %d", page2.Page, evmHistoryPage+1) } @@ -67,6 +69,7 @@ func testGetAddressTxsPaginationEVM(t *testing.T, h *TestHandler) { assertAddressMatches(t, page1.Address, address, "GetAddressTxsPaginationEVM.page1.address") assertPageMeta(t, page1.Page, page1.ItemsOnPage, page1.TotalPages, page1.Txs, "GetAddressTxsPaginationEVM.page1") + assertPageSizeUpperBound(t, len(page1.Transactions), page1.ItemsOnPage, evmHistoryPageSize, "GetAddressTxsPaginationEVM.page1.transactions") if len(page1.Transactions) == 0 { t.Fatalf("GetAddressTxsPaginationEVM page 1 returned no transactions") } @@ -81,6 +84,7 @@ func testGetAddressTxsPaginationEVM(t *testing.T, h *TestHandler) { assertAddressMatches(t, page2.Address, address, "GetAddressTxsPaginationEVM.page2.address") assertPageMeta(t, page2.Page, page2.ItemsOnPage, page2.TotalPages, page2.Txs, "GetAddressTxsPaginationEVM.page2") + assertPageSizeUpperBound(t, len(page2.Transactions), page2.ItemsOnPage, evmHistoryPageSize, "GetAddressTxsPaginationEVM.page2.transactions") if page2.Page != evmHistoryPage+1 { t.Fatalf("GetAddressTxsPaginationEVM page mismatch: got %d, want %d", page2.Page, evmHistoryPage+1) } diff --git a/tests/api/http_tests.go b/tests/api/http_tests.go index f06ed3db..6ed69ead 100644 --- a/tests/api/http_tests.go +++ b/tests/api/http_tests.go @@ -130,7 +130,7 @@ func testGetAddressTxids(t *testing.T, h *TestHandler) { var addr addressTxidsResponse h.mustGetJSON(t, path, &addr) - assertAddressTxidsPayload(t, &addr, address, txid, "GetAddressTxids") + assertAddressTxidsPayload(t, &addr, address, txid, "GetAddressTxids", addressPageSize) } func testGetAddressTxs(t *testing.T, h *TestHandler) { @@ -141,7 +141,7 @@ func testGetAddressTxs(t *testing.T, h *TestHandler) { var addr addressTxsResponse h.mustGetJSON(t, path, &addr) - assertAddressTxsPayload(t, &addr, address, txid, "GetAddressTxs") + assertAddressTxsPayload(t, &addr, address, txid, "GetAddressTxs", addressPageSize) } func testGetUtxo(t *testing.T, h *TestHandler) { @@ -155,18 +155,40 @@ func testGetUtxo(t *testing.T, h *TestHandler) { func testGetUtxoConfirmedFilter(t *testing.T, h *TestHandler) { address := h.sampleAddressOrSkip(t) - var all []utxoResponse - h.mustGetJSON(t, "/api/v2/utxo/"+url.PathEscape(address), &all) - var confirmed []utxoResponse h.mustGetJSON(t, "/api/v2/utxo/"+url.PathEscape(address)+"?confirmed=true", &confirmed) - if len(all) == 0 && len(confirmed) == 0 { + var all []utxoResponse + h.mustGetJSON(t, "/api/v2/utxo/"+url.PathEscape(address), &all) + + var explicitFalse []utxoResponse + h.mustGetJSON(t, "/api/v2/utxo/"+url.PathEscape(address)+"?confirmed=false", &explicitFalse) + + if len(all) == 0 && len(explicitFalse) == 0 && len(confirmed) == 0 { t.Skipf("Skipping test, address %s currently has no UTXOs", address) } assertUTXOListConfirmed(t, confirmed, "GetUtxoConfirmedFilter") assertUTXOList(t, all, "GetUtxoConfirmedFilter.all") + assertUTXOList(t, explicitFalse, "GetUtxoConfirmedFilter.confirmed=false") + + // confirmed=false should be equivalent to omitted confirmed query parameter. + // Retry once to reduce false positives from highly dynamic mempool state. + if !utxoSetsEqualByOutpoint(all, explicitFalse) { + var allRetry []utxoResponse + h.mustGetJSON(t, "/api/v2/utxo/"+url.PathEscape(address), &allRetry) + var explicitFalseRetry []utxoResponse + h.mustGetJSON(t, "/api/v2/utxo/"+url.PathEscape(address)+"?confirmed=false", &explicitFalseRetry) + assertUTXOList(t, allRetry, "GetUtxoConfirmedFilter.all.retry") + assertUTXOList(t, explicitFalseRetry, "GetUtxoConfirmedFilter.confirmed=false.retry") + assertUTXOSetsEqualByOutpoint(t, allRetry, explicitFalseRetry, "GetUtxoConfirmedFilter.default-vs-confirmed=false") + all = allRetry + explicitFalse = explicitFalseRetry + } + + // confirmed=false includes mempool effects, but any confirmed outpoint in that + // response must also exist in confirmed=true. + assertConfirmedUTXOsIncludedByOutpoint(t, explicitFalse, confirmed, "GetUtxoConfirmedFilter.confirmed-false-vs-true") } func (h *TestHandler) mustGetJSON(t *testing.T, path string, out interface{}) { diff --git a/tests/api/test_helpers.go b/tests/api/test_helpers.go index ac4a8514..59e2f709 100644 --- a/tests/api/test_helpers.go +++ b/tests/api/test_helpers.go @@ -27,17 +27,19 @@ func buildAddressDetailsPathWithTo(address, details string, page, pageSize, toHe return path } -func assertAddressTxidsPayload(t *testing.T, payload *addressTxidsResponse, address, txid, context string) { +func assertAddressTxidsPayload(t *testing.T, payload *addressTxidsResponse, address, txid, context string, pageSize int) { t.Helper() assertAddressMatches(t, payload.Address, address, context+".address") assertPageMeta(t, payload.Page, payload.ItemsOnPage, payload.TotalPages, payload.Txs, context) + assertPageSizeUpperBound(t, len(payload.Txids), payload.ItemsOnPage, pageSize, context+".txids") assertTxIDListContains(t, payload.Txids, txid, context+".txids") } -func assertAddressTxsPayload(t *testing.T, payload *addressTxsResponse, address, txid, context string) { +func assertAddressTxsPayload(t *testing.T, payload *addressTxsResponse, address, txid, context string, pageSize int) { t.Helper() assertAddressMatches(t, payload.Address, address, context+".address") assertPageMeta(t, payload.Page, payload.ItemsOnPage, payload.TotalPages, payload.Txs, context) + assertPageSizeUpperBound(t, len(payload.Transactions), payload.ItemsOnPage, pageSize, context+".transactions") assertTransactionsContainTxID(t, payload.Transactions, txid, context+".transactions") } @@ -79,6 +81,22 @@ func assertPageMetaAllowUnknownTotal(t *testing.T, page, itemsOnPage, totalPages } } +func assertPageSizeUpperBound(t *testing.T, payloadLen, itemsOnPage, requestedPageSize int, context string) { + t.Helper() + if requestedPageSize <= 0 { + return + } + if itemsOnPage > requestedPageSize { + t.Fatalf("%s invalid itemsOnPage %d > requested pageSize %d", context, itemsOnPage, requestedPageSize) + } + if payloadLen > requestedPageSize { + t.Fatalf("%s returned %d items, requested pageSize=%d", context, payloadLen, requestedPageSize) + } + if itemsOnPage > 0 && payloadLen > itemsOnPage { + t.Fatalf("%s returned %d items, greater than itemsOnPage=%d", context, payloadLen, itemsOnPage) + } +} + func assertTxIDListContains(t *testing.T, txids []string, txid, context string) { t.Helper() if len(txids) == 0 { @@ -137,6 +155,70 @@ func assertUTXOListNonNegativeConfirmations(t *testing.T, utxos []utxoResponse, } } +func assertUTXOSetsEqualByOutpoint(t *testing.T, got, want []utxoResponse, context string) { + t.Helper() + gotSet := utxoSetByOutpoint(t, got, context+".got") + wantSet := utxoSetByOutpoint(t, want, context+".want") + if len(gotSet) != len(wantSet) { + t.Fatalf("%s outpoint count mismatch: got=%d want=%d", context, len(gotSet), len(wantSet)) + } + for key := range wantSet { + if _, ok := gotSet[key]; !ok { + t.Fatalf("%s missing outpoint in got set: %s", context, key) + } + } +} + +func assertConfirmedUTXOsIncludedByOutpoint(t *testing.T, mixed, confirmed []utxoResponse, context string) { + t.Helper() + confirmedSet := utxoSetByOutpoint(t, confirmed, context+".confirmed") + for i := range mixed { + if isUnconfirmedUtxo(mixed[i]) { + continue + } + key := utxoOutpointKey(mixed[i]) + if _, ok := confirmedSet[key]; !ok { + t.Fatalf("%s missing confirmed outpoint %s in confirmed=true response", context, key) + } + } +} + +func utxoSetsEqualByOutpoint(a, b []utxoResponse) bool { + if len(a) != len(b) { + return false + } + set := make(map[string]struct{}, len(a)) + for i := range a { + set[utxoOutpointKey(a[i])] = struct{}{} + } + if len(set) != len(a) { + return false + } + for i := range b { + if _, ok := set[utxoOutpointKey(b[i])]; !ok { + return false + } + } + return true +} + +func utxoSetByOutpoint(t *testing.T, utxos []utxoResponse, context string) map[string]utxoResponse { + t.Helper() + set := make(map[string]utxoResponse, len(utxos)) + for i := range utxos { + key := utxoOutpointKey(utxos[i]) + if _, exists := set[key]; exists { + t.Fatalf("%s duplicate outpoint: %s", context, key) + } + set[key] = utxos[i] + } + return set +} + +func utxoOutpointKey(utxo utxoResponse) string { + return strings.ToLower(strings.TrimSpace(utxo.Txid)) + ":" + strconv.Itoa(utxo.Vout) +} + func assertEVMTokenBalancesPayload(t *testing.T, payload *evmAddressTokenBalanceResponse, address, context string) { t.Helper() assertAddressMatches(t, payload.Address, address, context+".address") diff --git a/tests/api/ws_tests.go b/tests/api/ws_tests.go index b29849ed..e374cd0e 100644 --- a/tests/api/ws_tests.go +++ b/tests/api/ws_tests.go @@ -66,7 +66,7 @@ func testWsGetAccountInfo(t *testing.T, h *TestHandler) { if err := json.Unmarshal(resp.Data, &info); err != nil { t.Fatalf("decode websocket getAccountInfo response: %v", err) } - assertAddressTxidsPayload(t, &info, address, txid, "WsGetAccountInfo") + assertAddressTxidsPayload(t, &info, address, txid, "WsGetAccountInfo", addressPageSize) } func testWsGetAccountUtxo(t *testing.T, h *TestHandler) {