mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
The primary-network chains answer to either case; one spelling beats two. /v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate. Co-authored-by: Hanzo Dev <[email protected]>
276 lines
7.0 KiB
Go
276 lines
7.0 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package e2e provides comprehensive end-to-end tests for the Lux indexer
|
|
// running against luxd in dev mode with K=1 consensus.
|
|
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func TestE2E(t *testing.T) {
|
|
RegisterFailHandler(Fail)
|
|
RunSpecs(t, "Lux Indexer E2E Suite")
|
|
}
|
|
|
|
// NodeConfig holds luxd configuration for E2E tests
|
|
type NodeConfig struct {
|
|
NetworkID string
|
|
HTTPPort int
|
|
StakingPort int
|
|
DataDir string
|
|
Mnemonic string
|
|
LogLevel string
|
|
PluginDir string
|
|
BootstrapTimeout time.Duration
|
|
NodeReadyTimeout time.Duration
|
|
}
|
|
|
|
// DefaultNodeConfig returns configuration for dev mode testing
|
|
func DefaultNodeConfig() *NodeConfig {
|
|
dataDir := os.Getenv("LUX_E2E_DATA_DIR")
|
|
if dataDir == "" {
|
|
dataDir = filepath.Join(os.TempDir(), "lux-e2e", fmt.Sprintf("run_%d", time.Now().UnixNano()))
|
|
}
|
|
|
|
mnemonic := os.Getenv("LUX_MNEMONIC")
|
|
if mnemonic == "" {
|
|
// Default test mnemonic (DO NOT USE IN PRODUCTION)
|
|
mnemonic = "test test test test test test test test test test test junk"
|
|
}
|
|
|
|
return &NodeConfig{
|
|
NetworkID: "1337", // Local dev network ID
|
|
HTTPPort: 19650, // Avoid conflict with running nodes
|
|
StakingPort: 19651,
|
|
DataDir: dataDir,
|
|
Mnemonic: mnemonic,
|
|
LogLevel: "info",
|
|
PluginDir: os.Getenv("LUX_PLUGIN_DIR"),
|
|
BootstrapTimeout: 60 * time.Second,
|
|
NodeReadyTimeout: 120 * time.Second,
|
|
}
|
|
}
|
|
|
|
// NodeProcess manages a luxd process for testing
|
|
type NodeProcess struct {
|
|
Config *NodeConfig
|
|
cmd *exec.Cmd
|
|
cancel context.CancelFunc
|
|
running bool
|
|
}
|
|
|
|
// Global node process for the test suite
|
|
var nodeProcess *NodeProcess
|
|
var nodeConfig *NodeConfig
|
|
|
|
var _ = BeforeSuite(func() {
|
|
// Skip if LUX_E2E_SKIP_NODE is set (use external node)
|
|
if os.Getenv("LUX_E2E_SKIP_NODE") != "" {
|
|
GinkgoWriter.Println("Using external node, skipping luxd start")
|
|
return
|
|
}
|
|
|
|
nodeConfig = DefaultNodeConfig()
|
|
|
|
// Ensure data directory exists
|
|
err := os.MkdirAll(nodeConfig.DataDir, 0755)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Check if luxd is available
|
|
luxdPath := findLuxd()
|
|
if luxdPath == "" {
|
|
Skip("luxd not found in PATH or NODE_PATH")
|
|
}
|
|
|
|
GinkgoWriter.Printf("Starting luxd in dev mode at %s\n", luxdPath)
|
|
GinkgoWriter.Printf("Data directory: %s\n", nodeConfig.DataDir)
|
|
|
|
// Start luxd in dev mode
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
args := []string{
|
|
"--dev", // Single-node K=1 consensus (implies --network-id=1337)
|
|
fmt.Sprintf("--http-port=%d", nodeConfig.HTTPPort),
|
|
fmt.Sprintf("--staking-port=%d", nodeConfig.StakingPort),
|
|
fmt.Sprintf("--db-dir=%s", filepath.Join(nodeConfig.DataDir, "db")),
|
|
fmt.Sprintf("--log-dir=%s", filepath.Join(nodeConfig.DataDir, "logs")),
|
|
"--log-level=info",
|
|
"--index-enabled=true",
|
|
}
|
|
|
|
if nodeConfig.PluginDir != "" {
|
|
args = append(args, fmt.Sprintf("--plugin-dir=%s", nodeConfig.PluginDir))
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, luxdPath, args...)
|
|
cmd.Stdout = GinkgoWriter
|
|
cmd.Stderr = GinkgoWriter
|
|
cmd.Env = append(os.Environ(),
|
|
fmt.Sprintf("LUX_MNEMONIC=%s", nodeConfig.Mnemonic),
|
|
)
|
|
|
|
err = cmd.Start()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
nodeProcess = &NodeProcess{
|
|
Config: nodeConfig,
|
|
cmd: cmd,
|
|
cancel: cancel,
|
|
running: true,
|
|
}
|
|
|
|
// Wait for node to be ready
|
|
GinkgoWriter.Println("Waiting for node to be ready...")
|
|
waitForNode(nodeConfig.HTTPPort, nodeConfig.NodeReadyTimeout)
|
|
GinkgoWriter.Println("Node is ready!")
|
|
})
|
|
|
|
var _ = AfterSuite(func() {
|
|
if nodeProcess != nil && nodeProcess.running {
|
|
GinkgoWriter.Println("Stopping luxd...")
|
|
nodeProcess.cancel()
|
|
|
|
// Wait for graceful shutdown
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- nodeProcess.cmd.Wait()
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
GinkgoWriter.Println("luxd stopped gracefully")
|
|
case <-time.After(30 * time.Second):
|
|
GinkgoWriter.Println("Forcing luxd termination...")
|
|
nodeProcess.cmd.Process.Kill()
|
|
}
|
|
|
|
nodeProcess.running = false
|
|
}
|
|
|
|
// Cleanup data directory if LUX_E2E_KEEP_DATA is not set
|
|
if os.Getenv("LUX_E2E_KEEP_DATA") == "" && nodeConfig != nil {
|
|
os.RemoveAll(nodeConfig.DataDir)
|
|
}
|
|
})
|
|
|
|
// findLuxd looks for luxd binary
|
|
func findLuxd() string {
|
|
// Check environment variable first
|
|
if path := os.Getenv("NODE_PATH"); path != "" {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path
|
|
}
|
|
}
|
|
|
|
// Check common locations
|
|
locations := []string{
|
|
"luxd",
|
|
"/usr/local/bin/luxd",
|
|
filepath.Join(os.Getenv("HOME"), "go/bin/luxd"),
|
|
filepath.Join(os.Getenv("HOME"), "work/lux/node/build/luxd"),
|
|
filepath.Join(os.Getenv("GOPATH"), "bin/luxd"),
|
|
}
|
|
|
|
for _, loc := range locations {
|
|
if path, err := exec.LookPath(loc); err == nil {
|
|
return path
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// waitForNode waits for the node to be ready
|
|
func waitForNode(port int, timeout time.Duration) {
|
|
deadline := time.Now().Add(timeout)
|
|
// Info API is at /v1/info
|
|
client := NewRPCClient(fmt.Sprintf("http://127.0.0.1:%d/v1/info", port))
|
|
|
|
for time.Now().Before(deadline) {
|
|
// Check bootstrap status via info API
|
|
_, err := client.CallInfo("info.isBootstrapped", map[string]string{"chain": "P"})
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
|
|
Fail(fmt.Sprintf("Node did not become ready within %v", timeout))
|
|
}
|
|
|
|
// GetRPCEndpoint returns the RPC endpoint for a chain
|
|
func GetRPCEndpoint(chain string) string {
|
|
// Check for external node via LUX_RPC_URL
|
|
if baseURL := os.Getenv("LUX_RPC_URL"); baseURL != "" {
|
|
// Extract host:port from base URL
|
|
// e.g., http://127.0.0.1:9650/v1/chain/c/rpc -> http://127.0.0.1:9650
|
|
parts := strings.Split(baseURL, "/v1/")
|
|
host := parts[0]
|
|
switch strings.ToUpper(chain) {
|
|
case "C":
|
|
return host + "/v1/chain/c/rpc"
|
|
case "P":
|
|
return host + "/v1/chain/p"
|
|
case "X":
|
|
return host + "/v1/chain/x"
|
|
default:
|
|
return host + "/v1/chain/" + chain + "/rpc"
|
|
}
|
|
}
|
|
|
|
port := 9650 // Default
|
|
if nodeConfig != nil {
|
|
port = nodeConfig.HTTPPort
|
|
}
|
|
|
|
switch strings.ToUpper(chain) {
|
|
case "C":
|
|
return fmt.Sprintf("http://127.0.0.1:%d/v1/chain/c/rpc", port)
|
|
case "P":
|
|
return fmt.Sprintf("http://127.0.0.1:%d/v1/chain/p", port)
|
|
case "X":
|
|
return fmt.Sprintf("http://127.0.0.1:%d/v1/chain/x", port)
|
|
default:
|
|
return fmt.Sprintf("http://127.0.0.1:%d/v1/chain/%s/rpc", port, chain)
|
|
}
|
|
}
|
|
|
|
// GetInfoEndpoint returns the info API endpoint
|
|
func GetInfoEndpoint() string {
|
|
if baseURL := os.Getenv("LUX_RPC_URL"); baseURL != "" {
|
|
parts := strings.Split(baseURL, "/v1/")
|
|
return parts[0] + "/v1/info"
|
|
}
|
|
port := 9650
|
|
if nodeConfig != nil {
|
|
port = nodeConfig.HTTPPort
|
|
}
|
|
return fmt.Sprintf("http://127.0.0.1:%d/v1/info", port)
|
|
}
|
|
|
|
// GetAdminEndpoint returns the admin API endpoint
|
|
func GetAdminEndpoint() string {
|
|
if baseURL := os.Getenv("LUX_RPC_URL"); baseURL != "" {
|
|
parts := strings.Split(baseURL, "/v1/")
|
|
return parts[0] + "/v1/admin"
|
|
}
|
|
port := 9650
|
|
if nodeConfig != nil {
|
|
port = nodeConfig.HTTPPort
|
|
}
|
|
return fmt.Sprintf("http://127.0.0.1:%d/v1/admin", port)
|
|
}
|