Files
indexer/explorer/graphql_test.go
T

339 lines
9.1 KiB
Go

package explorer
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"github.com/luxfi/indexer/explorer/testutil"
)
func TestGraphQLPlayground(t *testing.T) {
svc := testService(t)
mux := chi.NewRouter()
RegisterRoutes(mux, svc)
ts := httptest.NewServer(mux)
defer ts.Close()
t.Run("federated playground serves HTML on GET", func(t *testing.T) {
resp, err := http.Get(ts.URL + "/v1/explorer/graphql")
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "text/html") {
t.Errorf("Content-Type = %q, want text/html", ct)
}
body, _ := io.ReadAll(resp.Body)
html := string(body)
if !strings.Contains(html, "G-Chain Federated GraphQL") {
t.Error("playground should contain title 'G-Chain Federated GraphQL'")
}
if !strings.Contains(html, "endpoint: '/v1/explorer/graphql'") {
t.Error("playground endpoint should be /v1/explorer/graphql")
}
if !strings.Contains(html, "GraphQLPlayground.init") {
t.Error("playground should contain GraphQLPlayground.init")
}
})
t.Run("local playground serves HTML", func(t *testing.T) {
resp, err := http.Get(ts.URL + "/v1/explorer/local/graphql")
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
html := string(body)
if !strings.Contains(html, "Test Chain GraphQL") {
t.Error("playground should contain chain-specific title")
}
if !strings.Contains(html, "endpoint: '/v1/explorer/local/graphql'") {
t.Error("playground endpoint should be /v1/explorer/local/graphql")
}
})
}
func TestFederatedGraphQLProxy(t *testing.T) {
// Start a fake G-Chain endpoint that returns a known response.
fakeGChain := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"schemas": []map[string]string{
{"id": "default", "name": "Default Schema"},
},
},
})
}))
defer fakeGChain.Close()
svc := testService(t)
proxy, err := newGraphQLProxy(fakeGChain.URL)
if err != nil {
t.Fatalf("newGraphQLProxy: %v", err)
}
svc.gchainProxy = proxy
mux := chi.NewRouter()
RegisterRoutes(mux, svc)
ts := httptest.NewServer(mux)
defer ts.Close()
t.Run("POST proxies to G-Chain", func(t *testing.T) {
body := `{"query":"{ schemas { id name } }"}`
resp, err := http.Post(ts.URL+"/v1/explorer/graphql", "application/json", strings.NewReader(body))
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
data, ok := result["data"].(map[string]any)
if !ok {
t.Fatalf("response missing data field: %v", result)
}
schemas, ok := data["schemas"].([]any)
if !ok || len(schemas) == 0 {
t.Fatalf("expected schemas in response, got: %v", data)
}
})
}
func TestFederatedProxyUnavailable(t *testing.T) {
svc := testService(t)
svc.gchainProxy = nil // simulate not configured
mux := chi.NewRouter()
RegisterRoutes(mux, svc)
ts := httptest.NewServer(mux)
defer ts.Close()
resp, err := http.Post(ts.URL+"/v1/explorer/graphql", "application/json", strings.NewReader(`{"query":"{ schemas }"}`))
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusServiceUnavailable)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
errs, ok := result["errors"].([]any)
if !ok || len(errs) == 0 {
t.Error("expected GraphQL error in response")
}
}
func TestLocalGraphQLPost(t *testing.T) {
svc := testService(t)
mux := chi.NewRouter()
RegisterRoutes(mux, svc)
ts := httptest.NewServer(mux)
defer ts.Close()
resp, err := http.Post(ts.URL+"/v1/explorer/local/graphql", "application/json", strings.NewReader(`{"query":"{ __typename }"}`))
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
data, ok := result["data"].(map[string]any)
if !ok {
t.Fatalf("response missing data: %v", result)
}
if data["__typename"] != "Query" {
t.Errorf("__typename = %v, want Query", data["__typename"])
}
}
func TestCrossChainSearch(t *testing.T) {
// Create two separate SQLite DBs simulating two chains.
tdb1 := testutil.NewTestDB(t)
tdb2 := testutil.NewTestDB(t)
// Seed chain 1 with an address.
addr := testutil.DefaultAddress()
addr = tdb1.InsertAddress(t, addr)
// Seed chain 2 with a different address — should not match.
addr2 := testutil.DefaultAddress()
addr2.Hash = []byte{0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
addr2 = tdb2.InsertAddress(t, addr2)
// Open cross-chain DB connections (mirrors openIndexerDB behavior).
zoo, err := sql.Open("sqlite3",
fmt.Sprintf("file:%s?mode=ro&_journal_mode=WAL&_busy_timeout=5000&cache=shared", tdb2.Path))
if err != nil {
t.Fatal(err)
}
defer zoo.Close()
zoo.SetMaxOpenConns(2)
svc := &Service{
config: Config{
IndexerDBPath: tdb1.Path,
ChainID: testutil.DefaultChainID,
ChainName: "C-Chain",
CoinSymbol: "LUX",
CoinDecimals: 18,
ChainDBPaths: map[string]string{
"C-Chain": tdb1.Path,
"Zoo": tdb2.Path,
},
},
db: tdb1.DB,
chainDBs: map[string]*sql.DB{
"C-Chain": tdb1.DB,
"Zoo": zoo,
},
logger: testLogger(),
}
mux := chi.NewRouter()
RegisterRoutes(mux, svc)
ts := httptest.NewServer(mux)
defer ts.Close()
t.Run("requires address or tx_hash", func(t *testing.T) {
resp, err := http.Get(ts.URL + "/v1/explorer/search/cross-chain")
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusBadRequest)
}
})
t.Run("searches across chains by address", func(t *testing.T) {
addrHex := fmt.Sprintf("0x%x", addr.Hash)
resp, err := http.Get(ts.URL + "/v1/explorer/search/cross-chain?address=" + addrHex)
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
results, ok := result["results"].([]any)
if !ok {
t.Fatalf("expected results array, got %T: %v", result["results"], result)
}
// Should find results (at least from chains that have this address).
// The exact count depends on whether the address hash format matches.
if len(results) == 0 {
t.Log("no results returned (address format may not match raw bytes in SQLite)")
}
for _, r := range results {
entry := r.(map[string]any)
if entry["chain"] == nil {
t.Error("result missing chain field")
}
}
})
t.Run("single-chain fallback when no ChainDBPaths", func(t *testing.T) {
singleSvc := &Service{
config: Config{
IndexerDBPath: tdb1.Path,
ChainID: testutil.DefaultChainID,
ChainName: "C-Chain",
CoinSymbol: "LUX",
CoinDecimals: 18,
},
db: tdb1.DB,
logger: testLogger(),
}
singleMux := chi.NewRouter()
RegisterRoutes(singleMux, singleSvc)
singleTS := httptest.NewServer(singleMux)
defer singleTS.Close()
resp, err := http.Get(singleTS.URL + "/v1/explorer/search/cross-chain?address=0xdeadbeef")
if err != nil {
t.Fatalf("request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
}
})
}
func TestGraphqlErrorResponse(t *testing.T) {
resp := graphqlErrorResponse("test error")
if resp["data"] != nil {
t.Error("data should be nil")
}
errs, ok := resp["errors"].([]map[string]string)
if !ok {
t.Fatalf("errors should be []map[string]string, got %T", resp["errors"])
}
if len(errs) != 1 {
t.Fatalf("expected 1 error, got %d", len(errs))
}
if errs[0]["message"] != "test error" {
t.Errorf("message = %q, want %q", errs[0]["message"], "test error")
}
}
func TestNewGraphQLProxy(t *testing.T) {
t.Run("valid endpoint", func(t *testing.T) {
proxy, err := newGraphQLProxy("http://localhost:9650/v1/chain/G/graphql")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if proxy.target.Host != "localhost:9650" {
t.Errorf("host = %q, want localhost:9650", proxy.target.Host)
}
})
t.Run("invalid endpoint", func(t *testing.T) {
_, err := newGraphQLProxy("://bad")
if err == nil {
t.Error("expected error for invalid URL")
}
})
}