mirror of
https://github.com/trezor/blockbook.git
synced 2026-03-23 07:57:18 +01:00
ci: bb connectivity suite
This commit is contained in:
4
.github/workflows/testing.yml
vendored
4
.github/workflows/testing.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
run: make test
|
||||
|
||||
connectivity-tests:
|
||||
name: Backend Connectivity Tests
|
||||
name: Connectivity Tests
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
needs: unit-tests
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
vars_json: ${{ toJSON(vars) }}
|
||||
|
||||
- name: Run e2e tests
|
||||
run: make e2e ARGS="-v"
|
||||
run: make test-e2e ARGS="-v"
|
||||
|
||||
4
Makefile
4
Makefile
@@ -12,7 +12,7 @@ BB_RPC_ENV := $(shell env | awk -F= '/^BB_RPC_(URL_HTTP|URL_WS|BIND_HOST|ALLOW_I
|
||||
|
||||
TARGETS=$(subst .json,, $(shell ls configs/coins))
|
||||
|
||||
.PHONY: build build-debug test test-connectivity test-integration test-e2e e2e test-all deb
|
||||
.PHONY: build build-debug test test-connectivity test-integration test-e2e test-all deb
|
||||
|
||||
build: .bin-image
|
||||
docker run -t --rm -e PACKAGER=$(PACKAGER) $(BB_RPC_ENV) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(BIN_IMAGE) make build ARGS="$(ARGS)"
|
||||
@@ -29,8 +29,6 @@ test-integration: .bin-image
|
||||
test-e2e: .bin-image
|
||||
docker run -t --rm -e PACKAGER=$(PACKAGER) $(BB_RPC_ENV) -v "$(CURDIR):/src" --network="host" $(BIN_IMAGE) make test-e2e ARGS="$(ARGS)"
|
||||
|
||||
e2e: test-e2e
|
||||
|
||||
test-connectivity: .bin-image
|
||||
docker run -t --rm -e PACKAGER=$(PACKAGER) $(BB_RPC_ENV) -v "$(CURDIR):/src" --network="host" $(BIN_IMAGE) make test-connectivity ARGS="$(ARGS)"
|
||||
|
||||
|
||||
@@ -32,10 +32,8 @@ test-integration: prepare-sources
|
||||
test-e2e: prepare-sources
|
||||
cd $(BLOCKBOOK_SRC) && go test -tags 'integration' `go list github.com/trezor/blockbook/tests/...` -run 'TestIntegration/.*/api' -timeout 30m $(ARGS)
|
||||
|
||||
e2e: test-e2e
|
||||
|
||||
test-connectivity: prepare-sources
|
||||
cd $(BLOCKBOOK_SRC) && go test -tags 'integration' `go list github.com/trezor/blockbook/tests/...` -run 'TestIntegration/.*/connectivity' -timeout 30m $(ARGS)
|
||||
cd $(BLOCKBOOK_SRC) && go test -tags 'integration' github.com/trezor/blockbook/tests -run 'TestIntegration/.*/connectivity' -timeout 30m $(ARGS)
|
||||
|
||||
test-all: prepare-sources
|
||||
cd $(BLOCKBOOK_SRC) && go test -tags 'unittest integration' `go list ./... | grep -v '^github.com/trezor/blockbook/contrib'` -timeout 30m $(ARGS)
|
||||
|
||||
@@ -86,8 +86,15 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
HTTP connectivity for UTXO chains calls `getblockchaininfo`. For EVM chains it calls `web3_clientVersion`. WebSocket
|
||||
connectivity validates `web3_clientVersion` and opens a `newHeads` subscription.
|
||||
HTTP connectivity verifies both back-end and Blockbook accessibility:
|
||||
|
||||
* back-end: UTXO chains call `getblockchaininfo`, EVM chains call `web3_clientVersion`
|
||||
* Blockbook: calls `GET /api/status` (resolved from `BB_API_URL_HTTP_<coin alias>` or local `ports.blockbook_public`)
|
||||
|
||||
WebSocket connectivity also verifies both surfaces:
|
||||
|
||||
* back-end: validates `web3_clientVersion` and opens a `newHeads` subscription
|
||||
* Blockbook: connects to `/websocket` (or `BB_API_URL_WS_<coin alias>`) and calls `getInfo`
|
||||
|
||||
### Blockbook API end-to-end tests
|
||||
|
||||
|
||||
@@ -14,6 +14,16 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ResolveEndpoints resolves Blockbook API endpoints for a coin alias using
|
||||
// BB_API_URL_* overrides first and coin config fallbacks.
|
||||
func ResolveEndpoints(coin string) (string, string, error) {
|
||||
ep, err := resolveAPIEndpoints(coin)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return ep.HTTP, ep.WS, nil
|
||||
}
|
||||
|
||||
func resolveAPIEndpoints(coin string) (*apiEndpoints, error) {
|
||||
cfg, err := loadCoinConfig(coin)
|
||||
if err != nil {
|
||||
|
||||
247
tests/connectivity/blockbook_connectivity.go
Normal file
247
tests/connectivity/blockbook_connectivity.go
Normal file
@@ -0,0 +1,247 @@
|
||||
//go:build integration
|
||||
|
||||
package connectivity
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/trezor/blockbook/bchain"
|
||||
apitests "github.com/trezor/blockbook/tests/api"
|
||||
)
|
||||
|
||||
type blockbookStatusEnvelope struct {
|
||||
Blockbook json.RawMessage `json:"blockbook"`
|
||||
Backend json.RawMessage `json:"backend"`
|
||||
}
|
||||
|
||||
type blockbookWSRequest struct {
|
||||
ID string `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params interface{} `json:"params"`
|
||||
}
|
||||
|
||||
type blockbookWSResponse struct {
|
||||
ID string `json:"id"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type blockbookWSInfo struct {
|
||||
BestHeight int `json:"bestHeight"`
|
||||
BestHash string `json:"bestHash"`
|
||||
}
|
||||
|
||||
func BlockbookHTTPIntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, _ json.RawMessage) {
|
||||
t.Helper()
|
||||
|
||||
httpBase, _, err := apitests.ResolveEndpoints(coin)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve Blockbook endpoints for %s: %v", coin, err)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: connectivityTimeout,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
status, body, err := blockbookHTTPGet(client, httpBase, "/api/status")
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s/api/status: %v", httpBase, err)
|
||||
}
|
||||
if shouldUpgradeToHTTPS(status, body, httpBase) {
|
||||
if upgraded, ok := upgradeHTTPBaseToHTTPS(httpBase); ok {
|
||||
httpBase = upgraded
|
||||
status, body, err = blockbookHTTPGet(client, httpBase, "/api/status")
|
||||
if err != nil {
|
||||
t.Fatalf("GET %s/api/status: %v", httpBase, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("GET %s/api/status returned HTTP %d: %s", httpBase, status, previewBody(body))
|
||||
}
|
||||
|
||||
var envelope blockbookStatusEnvelope
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
t.Fatalf("decode %s/api/status: %v", httpBase, err)
|
||||
}
|
||||
if !hasNonEmptyJSON(envelope.Blockbook) {
|
||||
t.Fatalf("status response missing non-empty blockbook object")
|
||||
}
|
||||
if !hasNonEmptyJSON(envelope.Backend) {
|
||||
t.Fatalf("status response missing non-empty backend object")
|
||||
}
|
||||
}
|
||||
|
||||
func BlockbookWSIntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, _ json.RawMessage) {
|
||||
t.Helper()
|
||||
|
||||
_, wsURL, err := apitests.ResolveEndpoints(coin)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve Blockbook endpoints for %s: %v", coin, err)
|
||||
}
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: connectivityTimeout,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
conn, _, err := dialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
if upgraded, ok := upgradeWSBaseToWSS(wsURL); ok {
|
||||
conn, _, err = dialer.Dial(upgraded, nil)
|
||||
if err == nil {
|
||||
wsURL = upgraded
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial %s: %v", wsURL, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reqID := "connectivity-getinfo"
|
||||
req := blockbookWSRequest{
|
||||
ID: reqID,
|
||||
Method: "getInfo",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(connectivityTimeout))
|
||||
if err := conn.WriteJSON(&req); err != nil {
|
||||
t.Fatalf("websocket write getInfo: %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
conn.SetReadDeadline(time.Now().Add(connectivityTimeout))
|
||||
_, payload, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("websocket read getInfo: %v", err)
|
||||
}
|
||||
|
||||
var resp blockbookWSResponse
|
||||
if err := json.Unmarshal(payload, &resp); err != nil {
|
||||
t.Fatalf("decode websocket response: %v", err)
|
||||
}
|
||||
if resp.ID != reqID {
|
||||
continue
|
||||
}
|
||||
if msg, hasError := blockbookWebsocketError(resp.Data); hasError {
|
||||
t.Fatalf("websocket getInfo returned error: %s", msg)
|
||||
}
|
||||
|
||||
var info blockbookWSInfo
|
||||
if err := json.Unmarshal(resp.Data, &info); err != nil {
|
||||
t.Fatalf("decode websocket getInfo payload: %v", err)
|
||||
}
|
||||
if info.BestHeight < 0 {
|
||||
t.Fatalf("invalid websocket bestHeight: %d", info.BestHeight)
|
||||
}
|
||||
if strings.TrimSpace(info.BestHash) == "" {
|
||||
t.Fatalf("empty websocket bestHash")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatalf("missing websocket getInfo response for request id %s", reqID)
|
||||
}
|
||||
|
||||
func blockbookHTTPGet(client *http.Client, baseURL, path string) (int, []byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, resolveHTTPURL(baseURL, path), nil)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return resp.StatusCode, body, nil
|
||||
}
|
||||
|
||||
func resolveHTTPURL(baseURL, path string) string {
|
||||
if strings.HasPrefix(path, "/") {
|
||||
return baseURL + path
|
||||
}
|
||||
return baseURL + "/" + path
|
||||
}
|
||||
|
||||
func shouldUpgradeToHTTPS(status int, body []byte, baseURL string) bool {
|
||||
if status != http.StatusBadRequest {
|
||||
return false
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(string(body)), "http request to an https server") {
|
||||
return false
|
||||
}
|
||||
parsed, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return parsed.Scheme == "http"
|
||||
}
|
||||
|
||||
func upgradeHTTPBaseToHTTPS(raw string) (string, bool) {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Scheme != "http" {
|
||||
return "", false
|
||||
}
|
||||
u.Scheme = "https"
|
||||
return strings.TrimRight(u.String(), "/"), true
|
||||
}
|
||||
|
||||
func upgradeWSBaseToWSS(raw string) (string, bool) {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Scheme != "ws" {
|
||||
return "", false
|
||||
}
|
||||
u.Scheme = "wss"
|
||||
return u.String(), true
|
||||
}
|
||||
|
||||
func blockbookWebsocketError(data json.RawMessage) (string, bool) {
|
||||
var e struct {
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if e.Error == nil {
|
||||
return "", false
|
||||
}
|
||||
return e.Error.Message, true
|
||||
}
|
||||
|
||||
func hasNonEmptyJSON(raw json.RawMessage) bool {
|
||||
v := strings.TrimSpace(string(raw))
|
||||
return v != "" && v != "null" && v != "{}"
|
||||
}
|
||||
|
||||
func previewBody(body []byte) string {
|
||||
const max = 256
|
||||
s := strings.TrimSpace(string(body))
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + "..."
|
||||
}
|
||||
@@ -25,8 +25,8 @@ type connectivityCfg struct {
|
||||
}
|
||||
|
||||
// IntegrationTest runs connectivity checks for the requested modes (e.g., ["http","ws"]).
|
||||
// HTTP checks verify the backend responds (UTXO uses getblockchaininfo, EVM uses web3_clientVersion).
|
||||
// WS checks verify web3_clientVersion and a newHeads subscription over the WS endpoint.
|
||||
// HTTP mode verifies both backend RPC and Blockbook HTTP API accessibility.
|
||||
// WS mode verifies both backend WS RPC and Blockbook websocket accessibility.
|
||||
func IntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, testConfig json.RawMessage) {
|
||||
t.Helper()
|
||||
|
||||
@@ -39,8 +39,10 @@ func IntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Me
|
||||
switch mode {
|
||||
case "http":
|
||||
HTTPIntegrationTest(t, coin, nil, nil, nil)
|
||||
BlockbookHTTPIntegrationTest(t, coin, nil, nil, nil)
|
||||
case "ws":
|
||||
WSIntegrationTest(t, coin, nil, nil, nil)
|
||||
BlockbookWSIntegrationTest(t, coin, nil, nil, nil)
|
||||
default:
|
||||
t.Fatalf("unsupported connectivity mode %q for %s", mode, coin)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user