mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
Lux Partners Limited is not the entity behind this work. Every place the old name stood — copyright lines, site footers, brand config, legal and compliance text — now names the company that actually holds it. lux.fund is the exception and is not renamed to match: that site belongs to Lux Assets Management LLC, a different company, and it says so. Two kinds of line were left for a person rather than rewritten. A dated news item recording that Lux Partners Limited was established in the Isle of Man is a historical fact, and renaming it would falsify the record. And a sentence asserting that the company is domiciled in the Isle of Man is a claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle of Man company, so swapping the name there would assert something untrue. Co-authored-by: Hanzo Dev <[email protected]>
971 lines
23 KiB
Go
971 lines
23 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
// TestConfig for testing
|
|
type TestConfig struct {
|
|
HTTPPort int
|
|
ChainID int64
|
|
ChainName string
|
|
DatabaseURL string
|
|
}
|
|
|
|
func getTestConfig() TestConfig {
|
|
return TestConfig{
|
|
HTTPPort: 4001,
|
|
ChainID: 96369,
|
|
ChainName: "Lux C-Chain Test",
|
|
DatabaseURL: "postgres://localhost:5432/explorer_evm_test?sslmode=disable",
|
|
}
|
|
}
|
|
|
|
// MockDB creates an in-memory mock for testing without database
|
|
type MockDB struct{}
|
|
|
|
// setupTestServer creates a test server with mock data
|
|
func setupTestServer(t *testing.T) (*Server, *httptest.Server) {
|
|
cfg := Config{
|
|
HTTPPort: 4001,
|
|
ChainID: 96369,
|
|
ChainName: "Lux C-Chain Test",
|
|
}
|
|
|
|
// Create server with nil DB (handlers will need to handle this)
|
|
s := &Server{
|
|
config: cfg,
|
|
wsHub: NewWebSocketHub(),
|
|
repo: nil, // Mock repository
|
|
}
|
|
|
|
// Setup routes manually for testing
|
|
s.router = setupMockRoutes(s)
|
|
|
|
ts := httptest.NewServer(corsMiddleware(s.router))
|
|
return s, ts
|
|
}
|
|
|
|
func setupMockRoutes(s *Server) *mux.Router {
|
|
router := mux.NewRouter()
|
|
|
|
// Health endpoint
|
|
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "ok",
|
|
"chain_id": s.config.ChainID,
|
|
"chain_name": s.config.ChainName,
|
|
})
|
|
}).Methods("GET")
|
|
|
|
// Mock blocks endpoint
|
|
router.HandleFunc("/v1/explorer/blocks", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
blocks := []Block{
|
|
{
|
|
Hash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
ParentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
Height: 100,
|
|
Timestamp: time.Now(),
|
|
TransactionCount: 5,
|
|
GasUsed: "21000",
|
|
GasLimit: "8000000",
|
|
Size: 1024,
|
|
Miner: &Address{Hash: "0xdeadbeef00000000000000000000000000000001"},
|
|
},
|
|
}
|
|
json.NewEncoder(w).Encode(PaginatedResponse{Items: blocks})
|
|
}).Methods("GET")
|
|
|
|
// Mock single block endpoint
|
|
router.HandleFunc("/v1/explorer/blocks/{block_hash_or_number}", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
block := Block{
|
|
Hash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
ParentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
Height: 100,
|
|
Timestamp: time.Now(),
|
|
TransactionCount: 5,
|
|
}
|
|
json.NewEncoder(w).Encode(block)
|
|
}).Methods("GET")
|
|
|
|
// Mock transactions endpoint
|
|
router.HandleFunc("/v1/explorer/transactions", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
blockNum := uint64(100)
|
|
gasUsed := uint64(21000)
|
|
txIndex := 0
|
|
txs := []Transaction{
|
|
{
|
|
Hash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
|
BlockNumber: &blockNum,
|
|
From: &Address{Hash: "0x1111111111111111111111111111111111111111"},
|
|
To: &Address{Hash: "0x2222222222222222222222222222222222222222"},
|
|
Value: "1000000000000000000",
|
|
Gas: 21000,
|
|
GasPrice: "20000000000",
|
|
GasUsed: &gasUsed,
|
|
TransactionIndex: &txIndex,
|
|
Status: "ok",
|
|
},
|
|
}
|
|
json.NewEncoder(w).Encode(PaginatedResponse{Items: txs})
|
|
}).Methods("GET")
|
|
|
|
// Mock addresses endpoint
|
|
router.HandleFunc("/v1/explorer/addresses/{address_hash}", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
txCount := uint64(10)
|
|
addr := Address{
|
|
Hash: "0x1111111111111111111111111111111111111111",
|
|
Balance: "1000000000000000000",
|
|
TransactionCount: &txCount,
|
|
IsContract: false,
|
|
}
|
|
json.NewEncoder(w).Encode(addr)
|
|
}).Methods("GET")
|
|
|
|
// Mock tokens endpoint
|
|
router.HandleFunc("/v1/explorer/tokens", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
decimals := uint8(18)
|
|
holderCount := uint64(1000)
|
|
tokens := []Token{
|
|
{
|
|
Address: "0x3333333333333333333333333333333333333333",
|
|
Name: "Test Token",
|
|
Symbol: "TEST",
|
|
Decimals: &decimals,
|
|
TotalSupply: "1000000000000000000000000",
|
|
Type: "ERC-20",
|
|
HolderCount: &holderCount,
|
|
},
|
|
}
|
|
json.NewEncoder(w).Encode(PaginatedResponse{Items: tokens})
|
|
}).Methods("GET")
|
|
|
|
// Mock search endpoint
|
|
router.HandleFunc("/v1/explorer/search", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
query := r.URL.Query().Get("q")
|
|
var results []SearchResult
|
|
|
|
if strings.HasPrefix(query, "0x") && len(query) == 42 {
|
|
results = append(results, SearchResult{
|
|
Type: "address",
|
|
Address: query,
|
|
})
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(PaginatedResponse{Items: results})
|
|
}).Methods("GET")
|
|
|
|
// Mock stats endpoint
|
|
router.HandleFunc("/v1/explorer/stats", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
stats := ChainStats{
|
|
TotalBlocks: 100,
|
|
TotalTransactions: 500,
|
|
TotalAddresses: 50,
|
|
AverageBlockTime: 2.0,
|
|
}
|
|
json.NewEncoder(w).Encode(stats)
|
|
}).Methods("GET")
|
|
|
|
// Mock RPC endpoint
|
|
router.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
module := r.URL.Query().Get("module")
|
|
action := r.URL.Query().Get("action")
|
|
|
|
switch module {
|
|
case "account":
|
|
if action == "balance" {
|
|
json.NewEncoder(w).Encode(RPCResponse{
|
|
Status: "1",
|
|
Message: "OK",
|
|
Result: "1000000000000000000",
|
|
})
|
|
} else if action == "txlist" {
|
|
json.NewEncoder(w).Encode(RPCResponse{
|
|
Status: "1",
|
|
Message: "OK",
|
|
Result: []interface{}{},
|
|
})
|
|
}
|
|
case "stats":
|
|
json.NewEncoder(w).Encode(RPCResponse{
|
|
Status: "1",
|
|
Message: "OK",
|
|
Result: "0",
|
|
})
|
|
default:
|
|
json.NewEncoder(w).Encode(RPCResponse{
|
|
Status: "0",
|
|
Message: "NOTOK",
|
|
Result: "Invalid module",
|
|
})
|
|
}
|
|
}).Methods("GET")
|
|
|
|
// Mock GraphQL endpoint
|
|
router.HandleFunc("/v1/explorer/graphql", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "GET" {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write([]byte("<html>GraphQL Playground</html>"))
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
var req GraphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
json.NewEncoder(w).Encode(GraphQLResponse{
|
|
Errors: []GraphQLError{{Message: "Invalid request"}},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Return mock data based on query
|
|
json.NewEncoder(w).Encode(GraphQLResponse{
|
|
Data: map[string]interface{}{
|
|
"stats": map[string]interface{}{
|
|
"totalBlocks": 100,
|
|
"totalTransactions": 500,
|
|
},
|
|
},
|
|
})
|
|
}).Methods("GET", "POST")
|
|
|
|
return router
|
|
}
|
|
|
|
// Tests
|
|
|
|
func TestHealthEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/health")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if result["status"] != "ok" {
|
|
t.Errorf("Expected status 'ok', got %v", result["status"])
|
|
}
|
|
|
|
if result["chain_id"].(float64) != 96369 {
|
|
t.Errorf("Expected chain_id 96369, got %v", result["chain_id"])
|
|
}
|
|
}
|
|
|
|
func TestBlocksEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/blocks")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result PaginatedResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
blocks, ok := result.Items.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("Expected items to be a slice")
|
|
}
|
|
|
|
if len(blocks) == 0 {
|
|
t.Error("Expected at least one block")
|
|
}
|
|
}
|
|
|
|
func TestSingleBlockEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/blocks/100")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var block Block
|
|
if err := json.NewDecoder(resp.Body).Decode(&block); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if block.Height != 100 {
|
|
t.Errorf("Expected block height 100, got %d", block.Height)
|
|
}
|
|
}
|
|
|
|
func TestTransactionsEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/transactions")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result PaginatedResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if result.Items == nil {
|
|
t.Error("Expected items in response")
|
|
}
|
|
}
|
|
|
|
func TestAddressEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/addresses/0x1111111111111111111111111111111111111111")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var addr Address
|
|
if err := json.NewDecoder(resp.Body).Decode(&addr); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if addr.Hash == "" {
|
|
t.Error("Expected address hash in response")
|
|
}
|
|
}
|
|
|
|
func TestTokensEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/tokens")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result PaginatedResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if result.Items == nil {
|
|
t.Error("Expected items in response")
|
|
}
|
|
}
|
|
|
|
func TestSearchEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
tests := []struct {
|
|
query string
|
|
expected string
|
|
}{
|
|
{"0x1111111111111111111111111111111111111111", "address"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/search?q=" + test.query)
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result PaginatedResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
func TestStatsEndpoint(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/stats")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var stats ChainStats
|
|
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if stats.TotalBlocks == 0 {
|
|
t.Error("Expected non-zero total blocks")
|
|
}
|
|
}
|
|
|
|
// RPC API Tests
|
|
|
|
func TestRPCAccountBalance(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/api?module=account&action=balance&address=0x1111111111111111111111111111111111111111")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result RPCResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if result.Status != "1" {
|
|
t.Errorf("Expected status '1', got %s", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestRPCAccountTxList(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/api?module=account&action=txlist&address=0x1111111111111111111111111111111111111111")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result RPCResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if result.Status != "1" {
|
|
t.Errorf("Expected status '1', got %s", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestRPCInvalidModule(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/api?module=invalid&action=test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result RPCResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if result.Status != "0" {
|
|
t.Errorf("Expected status '0' for invalid module, got %s", result.Status)
|
|
}
|
|
}
|
|
|
|
// GraphQL Tests
|
|
|
|
func TestGraphQLPlayground(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/graphql")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if !strings.Contains(contentType, "text/html") {
|
|
t.Errorf("Expected text/html content type, got %s", contentType)
|
|
}
|
|
}
|
|
|
|
func TestGraphQLQuery(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
query := GraphQLRequest{
|
|
Query: `query { stats { totalBlocks totalTransactions } }`,
|
|
}
|
|
|
|
body, _ := json.Marshal(query)
|
|
resp, err := http.Post(ts.URL+"/v1/explorer/graphql", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result GraphQLResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if result.Data == nil {
|
|
t.Error("Expected data in response")
|
|
}
|
|
}
|
|
|
|
// Pagination Tests
|
|
|
|
func TestPaginationParameters(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
tests := []struct {
|
|
url string
|
|
name string
|
|
}{
|
|
{"/v1/explorer/blocks?page=0&page_size=10", "blocks with page params"},
|
|
{"/v1/explorer/transactions?page=0&page_size=10", "transactions with page params"},
|
|
{"/v1/explorer/tokens?page=0&page_size=10", "tokens with page params"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
resp, err := http.Get(ts.URL + test.url)
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var result PaginatedResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// CORS Tests
|
|
|
|
func TestCORSHeaders(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
resp, err := http.Get(ts.URL + "/health")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
cors := resp.Header.Get("Access-Control-Allow-Origin")
|
|
if cors != "*" {
|
|
t.Errorf("Expected CORS header '*', got '%s'", cors)
|
|
}
|
|
}
|
|
|
|
func TestOptionsRequest(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
req, _ := http.NewRequest("OPTIONS", ts.URL+"/v1/explorer/blocks", nil)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// Type Tests
|
|
|
|
func TestBlockSerialization(t *testing.T) {
|
|
block := Block{
|
|
Hash: "0x123",
|
|
ParentHash: "0x000",
|
|
Height: 100,
|
|
Timestamp: time.Now(),
|
|
TransactionCount: 5,
|
|
GasUsed: "21000",
|
|
GasLimit: "8000000",
|
|
Size: 1024,
|
|
}
|
|
|
|
data, err := json.Marshal(block)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal block: %v", err)
|
|
}
|
|
|
|
var decoded Block
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("Failed to unmarshal block: %v", err)
|
|
}
|
|
|
|
if decoded.Height != block.Height {
|
|
t.Errorf("Height mismatch: expected %d, got %d", block.Height, decoded.Height)
|
|
}
|
|
}
|
|
|
|
func TestTransactionSerialization(t *testing.T) {
|
|
blockNum := uint64(100)
|
|
gasUsed := uint64(21000)
|
|
txIndex := 0
|
|
|
|
tx := Transaction{
|
|
Hash: "0xabc",
|
|
BlockNumber: &blockNum,
|
|
From: &Address{Hash: "0x111"},
|
|
To: &Address{Hash: "0x222"},
|
|
Value: "1000000000000000000",
|
|
Gas: 21000,
|
|
GasPrice: "20000000000",
|
|
GasUsed: &gasUsed,
|
|
TransactionIndex: &txIndex,
|
|
Status: "ok",
|
|
}
|
|
|
|
data, err := json.Marshal(tx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal transaction: %v", err)
|
|
}
|
|
|
|
var decoded Transaction
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("Failed to unmarshal transaction: %v", err)
|
|
}
|
|
|
|
if decoded.Hash != tx.Hash {
|
|
t.Errorf("Hash mismatch: expected %s, got %s", tx.Hash, decoded.Hash)
|
|
}
|
|
}
|
|
|
|
func TestAddressSerialization(t *testing.T) {
|
|
txCount := uint64(10)
|
|
|
|
addr := Address{
|
|
Hash: "0x1111111111111111111111111111111111111111",
|
|
Balance: "1000000000000000000",
|
|
TransactionCount: &txCount,
|
|
IsContract: false,
|
|
}
|
|
|
|
data, err := json.Marshal(addr)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal address: %v", err)
|
|
}
|
|
|
|
var decoded Address
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("Failed to unmarshal address: %v", err)
|
|
}
|
|
|
|
if decoded.Hash != addr.Hash {
|
|
t.Errorf("Hash mismatch: expected %s, got %s", addr.Hash, decoded.Hash)
|
|
}
|
|
}
|
|
|
|
func TestTokenSerialization(t *testing.T) {
|
|
decimals := uint8(18)
|
|
holderCount := uint64(1000)
|
|
|
|
token := Token{
|
|
Address: "0x3333333333333333333333333333333333333333",
|
|
Name: "Test Token",
|
|
Symbol: "TEST",
|
|
Decimals: &decimals,
|
|
TotalSupply: "1000000000000000000000000",
|
|
Type: "ERC-20",
|
|
HolderCount: &holderCount,
|
|
}
|
|
|
|
data, err := json.Marshal(token)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal token: %v", err)
|
|
}
|
|
|
|
var decoded Token
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("Failed to unmarshal token: %v", err)
|
|
}
|
|
|
|
if decoded.Symbol != token.Symbol {
|
|
t.Errorf("Symbol mismatch: expected %s, got %s", token.Symbol, decoded.Symbol)
|
|
}
|
|
}
|
|
|
|
// Benchmarks
|
|
|
|
func BenchmarkHealthEndpoint(b *testing.B) {
|
|
_, ts := setupTestServer(nil)
|
|
defer ts.Close()
|
|
|
|
client := &http.Client{}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
resp, err := client.Get(ts.URL + "/health")
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
io.Copy(io.Discard, resp.Body)
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
func BenchmarkBlocksEndpoint(b *testing.B) {
|
|
_, ts := setupTestServer(nil)
|
|
defer ts.Close()
|
|
|
|
client := &http.Client{}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
resp, err := client.Get(ts.URL + "/v1/explorer/blocks")
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
io.Copy(io.Discard, resp.Body)
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
func BenchmarkJSONSerialization(b *testing.B) {
|
|
block := Block{
|
|
Hash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
ParentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
Height: 100,
|
|
Timestamp: time.Now(),
|
|
TransactionCount: 5,
|
|
GasUsed: "21000",
|
|
GasLimit: "8000000",
|
|
Size: 1024,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := json.Marshal(block)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Integration test with real database (skipped by default)
|
|
func TestIntegrationWithDatabase(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
cfg := getTestConfig()
|
|
db, err := sql.Open("postgres", cfg.DatabaseURL)
|
|
if err != nil {
|
|
t.Skipf("Database not available: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := db.Ping(); err != nil {
|
|
t.Skipf("Database not reachable: %v", err)
|
|
}
|
|
|
|
server := NewServer(Config{
|
|
HTTPPort: cfg.HTTPPort,
|
|
ChainID: cfg.ChainID,
|
|
ChainName: cfg.ChainName,
|
|
}, db)
|
|
|
|
ts := httptest.NewServer(corsMiddleware(server.Router()))
|
|
defer ts.Close()
|
|
|
|
// Test real database queries
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/stats")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
t.Errorf("Expected status 200, got %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
}
|
|
|
|
// WebSocket test helper
|
|
func TestWebSocketHub(t *testing.T) {
|
|
hub := NewWebSocketHub()
|
|
|
|
// Test stats
|
|
stats := hub.Stats()
|
|
if stats["total_connections"].(int) != 0 {
|
|
t.Errorf("Expected 0 connections, got %v", stats["total_connections"])
|
|
}
|
|
}
|
|
|
|
// Test block broadcast
|
|
func TestBroadcastBlock(t *testing.T) {
|
|
hub := NewWebSocketHub()
|
|
|
|
block := &Block{
|
|
Hash: "0x123",
|
|
Height: 100,
|
|
TransactionCount: 5,
|
|
}
|
|
|
|
// Should not panic with no clients
|
|
hub.BroadcastBlock(block)
|
|
}
|
|
|
|
// Test transaction broadcast
|
|
func TestBroadcastTransaction(t *testing.T) {
|
|
hub := NewWebSocketHub()
|
|
|
|
tx := &Transaction{
|
|
Hash: "0xabc",
|
|
From: &Address{Hash: "0x111"},
|
|
To: &Address{Hash: "0x222"},
|
|
Value: "1000",
|
|
}
|
|
|
|
// Should not panic with no clients
|
|
hub.BroadcastTransaction(tx)
|
|
}
|
|
|
|
// Test token transfer broadcast
|
|
func TestBroadcastTokenTransfer(t *testing.T) {
|
|
hub := NewWebSocketHub()
|
|
|
|
decimals := uint8(18)
|
|
transfer := &TokenTransfer{
|
|
TxHash: "0xdef",
|
|
From: &Address{Hash: "0x111"},
|
|
To: &Address{Hash: "0x222"},
|
|
Token: &Token{
|
|
Address: "0x333",
|
|
Symbol: "TEST",
|
|
Decimals: &decimals,
|
|
},
|
|
Total: &TokenTotal{Value: "1000"},
|
|
}
|
|
|
|
// Should not panic with no clients
|
|
hub.BroadcastTokenTransfer(transfer)
|
|
}
|
|
|
|
// Test multiple API calls
|
|
func TestAPISequence(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
// Get stats first
|
|
resp, _ := http.Get(ts.URL + "/v1/explorer/stats")
|
|
resp.Body.Close()
|
|
|
|
// Then get blocks
|
|
resp, _ = http.Get(ts.URL + "/v1/explorer/blocks")
|
|
resp.Body.Close()
|
|
|
|
// Then get transactions
|
|
resp, _ = http.Get(ts.URL + "/v1/explorer/transactions")
|
|
resp.Body.Close()
|
|
|
|
// Then search
|
|
resp, _ = http.Get(ts.URL + "/v1/explorer/search?q=0x123")
|
|
resp.Body.Close()
|
|
}
|
|
|
|
// Parallel test
|
|
func TestParallelRequests(t *testing.T) {
|
|
_, ts := setupTestServer(t)
|
|
defer ts.Close()
|
|
|
|
t.Run("parallel", func(t *testing.T) {
|
|
for i := 0; i < 10; i++ {
|
|
t.Run(fmt.Sprintf("request_%d", i), func(t *testing.T) {
|
|
t.Parallel()
|
|
resp, err := http.Get(ts.URL + "/v1/explorer/blocks")
|
|
if err != nil {
|
|
t.Fatalf("Request failed: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
})
|
|
}
|
|
})
|
|
}
|