2026-03-28 13:40:32 -07:00
package rest
import (
"encoding/json"
"net/http"
"net/http/httptest"
2026-04-07 23:22:12 -07:00
"os"
"path/filepath"
2026-03-28 13:40:32 -07:00
"testing"
)
type testNetworksCatalog struct {
Name string ` json:"name" `
DefaultChainID int ` json:"defaultChainId" `
ExplorerURL string ` json:"explorerUrl" `
TokenListURL string ` json:"tokenListUrl" `
GeneratedBy string ` json:"generatedBy" `
Chains [ ] struct {
ChainID string ` json:"chainId" `
ChainIDDecimal int ` json:"chainIdDecimal" `
ChainName string ` json:"chainName" `
ShortName string ` json:"shortName" `
RPCURLs [ ] string ` json:"rpcUrls" `
BlockExplorerURL [ ] string ` json:"blockExplorerUrls" `
InfoURL string ` json:"infoURL" `
ExplorerAPIURL string ` json:"explorerApiUrl" `
Testnet bool ` json:"testnet" `
} ` json:"chains" `
}
type testTokenList struct {
Name string ` json:"name" `
Keywords [ ] string
Extensions struct {
DefaultChainID int ` json:"defaultChainId" `
ExplorerURL string ` json:"explorerUrl" `
NetworksConfigURL string ` json:"networksConfigUrl" `
} ` json:"extensions" `
Tokens [ ] struct {
ChainID int ` json:"chainId" `
Address string ` json:"address" `
Symbol string ` json:"symbol" `
Decimals int ` json:"decimals" `
Extensions struct {
UnitOfAccount string ` json:"unitOfAccount" `
UnitDescription string ` json:"unitDescription" `
} ` json:"extensions" `
} ` json:"tokens" `
}
2026-03-28 15:56:42 -07:00
type testCapabilitiesCatalog struct {
Name string ` json:"name" `
GeneratedBy string ` json:"generatedBy" `
ChainID int ` json:"chainId" `
ChainName string ` json:"chainName" `
RPCURL string ` json:"rpcUrl" `
HTTP struct {
SupportedMethods [ ] string ` json:"supportedMethods" `
UnsupportedMethods [ ] string ` json:"unsupportedMethods" `
Notes [ ] string ` json:"notes" `
} ` json:"http" `
Tracing struct {
SupportedMethods [ ] string ` json:"supportedMethods" `
UnsupportedMethods [ ] string ` json:"unsupportedMethods" `
Notes [ ] string ` json:"notes" `
} ` json:"tracing" `
WalletSupport struct {
WalletAddEthereumChain bool ` json:"walletAddEthereumChain" `
WalletWatchAsset bool ` json:"walletWatchAsset" `
Notes [ ] string ` json:"notes" `
} ` json:"walletSupport" `
}
2026-03-28 13:40:32 -07:00
func setupConfigHandler ( ) http . Handler {
server := NewServer ( nil , 138 )
mux := http . NewServeMux ( )
server . SetupRoutes ( mux )
return server . addMiddleware ( mux )
}
2026-03-28 15:56:42 -07:00
func containsString ( items [ ] string , want string ) bool {
for _ , item := range items {
if item == want {
return true
}
}
return false
}
2026-03-28 13:40:32 -07:00
func TestConfigNetworksEndpointProvidesWalletMetadata ( t * testing . T ) {
handler := setupConfigHandler ( )
req := httptest . NewRequest ( http . MethodGet , "/api/config/networks" , nil )
w := httptest . NewRecorder ( )
handler . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d" , w . Code )
}
if got := w . Header ( ) . Get ( "Access-Control-Allow-Origin" ) ; got == "" {
t . Fatal ( "expected CORS header on config endpoint" )
}
var payload testNetworksCatalog
if err := json . Unmarshal ( w . Body . Bytes ( ) , & payload ) ; err != nil {
t . Fatalf ( "failed to parse networks payload: %v" , err )
}
if payload . DefaultChainID != 138 {
t . Fatalf ( "expected defaultChainId 138, got %d" , payload . DefaultChainID )
}
if payload . ExplorerURL == "" || payload . TokenListURL == "" || payload . GeneratedBy == "" {
t . Fatal ( "expected root metadata fields to be populated" )
}
if len ( payload . Chains ) < 3 {
t . Fatalf ( "expected multiple chain entries, got %d" , len ( payload . Chains ) )
}
var foundChain138 bool
for _ , chain := range payload . Chains {
if chain . ChainIDDecimal != 138 {
continue
}
foundChain138 = true
if chain . ShortName == "" || chain . InfoURL == "" || chain . ExplorerAPIURL == "" {
t . Fatal ( "expected Chain 138 optional metadata to be populated" )
}
if len ( chain . RPCURLs ) == 0 || len ( chain . BlockExplorerURL ) == 0 {
t . Fatal ( "expected Chain 138 RPC and explorer URLs" )
}
if chain . Testnet {
t . Fatal ( "expected Chain 138 to be marked as mainnet" )
}
}
if ! foundChain138 {
t . Fatal ( "expected Chain 138 entry in networks catalog" )
}
}
func TestConfigTokenListEndpointProvidesOptionalMetadata ( t * testing . T ) {
handler := setupConfigHandler ( )
req := httptest . NewRequest ( http . MethodGet , "/api/config/token-list" , nil )
w := httptest . NewRecorder ( )
handler . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d" , w . Code )
}
var payload testTokenList
if err := json . Unmarshal ( w . Body . Bytes ( ) , & payload ) ; err != nil {
t . Fatalf ( "failed to parse token list payload: %v" , err )
}
if len ( payload . Keywords ) == 0 {
t . Fatal ( "expected token list keywords" )
}
if payload . Extensions . DefaultChainID != 138 || payload . Extensions . ExplorerURL == "" || payload . Extensions . NetworksConfigURL == "" {
t . Fatal ( "expected root-level token list extensions" )
}
var foundCXAUC bool
var foundCUSDT bool
for _ , token := range payload . Tokens {
switch token . Symbol {
case "cXAUC" :
foundCXAUC = true
if token . Extensions . UnitOfAccount == "" || token . Extensions . UnitDescription == "" {
t . Fatal ( "expected cXAUC optional unit metadata" )
}
case "cUSDT" :
foundCUSDT = true
if token . Decimals != 6 {
t . Fatalf ( "expected cUSDT decimals 6, got %d" , token . Decimals )
}
}
}
if ! foundCXAUC || ! foundCUSDT {
t . Fatal ( "expected cXAUC and cUSDT in token list" )
}
}
2026-03-28 15:56:42 -07:00
func TestConfigCapabilitiesEndpointProvidesRPCCapabilityMatrix ( t * testing . T ) {
handler := setupConfigHandler ( )
req := httptest . NewRequest ( http . MethodGet , "/api/config/capabilities" , nil )
w := httptest . NewRecorder ( )
handler . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d" , w . Code )
}
var payload testCapabilitiesCatalog
if err := json . Unmarshal ( w . Body . Bytes ( ) , & payload ) ; err != nil {
t . Fatalf ( "failed to parse capabilities payload: %v" , err )
}
if payload . ChainID != 138 || payload . ChainName == "" || payload . RPCURL == "" || payload . GeneratedBy == "" {
t . Fatal ( "expected populated chain-level capability metadata" )
}
if ! payload . WalletSupport . WalletAddEthereumChain || ! payload . WalletSupport . WalletWatchAsset {
t . Fatal ( "expected wallet support flags to be true" )
}
if ! containsString ( payload . HTTP . SupportedMethods , "eth_feeHistory" ) {
t . Fatal ( "expected eth_feeHistory support to be documented" )
}
2026-04-07 23:22:12 -07:00
if ! containsString ( payload . HTTP . SupportedMethods , "eth_maxPriorityFeePerGas" ) {
t . Fatal ( "expected eth_maxPriorityFeePerGas support to be documented" )
2026-03-28 15:56:42 -07:00
}
if ! containsString ( payload . Tracing . SupportedMethods , "trace_block" ) {
t . Fatal ( "expected trace_block support to be documented" )
}
}
2026-04-07 23:22:12 -07:00
func TestConfigTokenListEndpointReloadsRuntimeFileWithoutRestart ( t * testing . T ) {
dir := t . TempDir ( )
file := filepath . Join ( dir , "token-list.json" )
first := ` { "name":"Runtime Token List v1","tokens":[ { "chainId":138,"address":"0x1111111111111111111111111111111111111111","symbol":"RT1","name":"Runtime One","decimals":6}]} `
second := ` { "name":"Runtime Token List v2","tokens":[ { "chainId":138,"address":"0x2222222222222222222222222222222222222222","symbol":"RT2","name":"Runtime Two","decimals":6}]} `
if err := os . WriteFile ( file , [ ] byte ( first ) , 0 o644 ) ; err != nil {
t . Fatalf ( "failed to write initial runtime file: %v" , err )
}
t . Setenv ( "CONFIG_TOKEN_LIST_JSON_PATH" , file )
handler := setupConfigHandler ( )
req1 := httptest . NewRequest ( http . MethodGet , "/api/config/token-list" , nil )
w1 := httptest . NewRecorder ( )
handler . ServeHTTP ( w1 , req1 )
if w1 . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d" , w1 . Code )
}
if got := w1 . Header ( ) . Get ( "X-Config-Source" ) ; got != "runtime-file" {
t . Fatalf ( "expected runtime-file config source, got %q" , got )
}
etag1 := w1 . Header ( ) . Get ( "ETag" )
if etag1 == "" {
t . Fatal ( "expected ETag header on runtime-backed response" )
}
var body1 testTokenList
if err := json . Unmarshal ( w1 . Body . Bytes ( ) , & body1 ) ; err != nil {
t . Fatalf ( "failed to parse runtime token list v1: %v" , err )
}
if body1 . Name != "Runtime Token List v1" {
t . Fatalf ( "expected first runtime payload, got %q" , body1 . Name )
}
if err := os . WriteFile ( file , [ ] byte ( second ) , 0 o644 ) ; err != nil {
t . Fatalf ( "failed to write updated runtime file: %v" , err )
}
req2 := httptest . NewRequest ( http . MethodGet , "/api/config/token-list" , nil )
w2 := httptest . NewRecorder ( )
handler . ServeHTTP ( w2 , req2 )
if w2 . Code != http . StatusOK {
t . Fatalf ( "expected 200 after runtime update, got %d" , w2 . Code )
}
if got := w2 . Header ( ) . Get ( "ETag" ) ; got == "" || got == etag1 {
t . Fatalf ( "expected changed ETag after runtime update, got %q" , got )
}
var body2 testTokenList
if err := json . Unmarshal ( w2 . Body . Bytes ( ) , & body2 ) ; err != nil {
t . Fatalf ( "failed to parse runtime token list v2: %v" , err )
}
if body2 . Name != "Runtime Token List v2" {
t . Fatalf ( "expected updated runtime payload, got %q" , body2 . Name )
}
}
func TestConfigTokenListEndpointSupportsETagRevalidation ( t * testing . T ) {
handler := setupConfigHandler ( )
req1 := httptest . NewRequest ( http . MethodGet , "/api/config/token-list" , nil )
w1 := httptest . NewRecorder ( )
handler . ServeHTTP ( w1 , req1 )
if w1 . Code != http . StatusOK {
t . Fatalf ( "expected 200, got %d" , w1 . Code )
}
etag := w1 . Header ( ) . Get ( "ETag" )
if etag == "" {
t . Fatal ( "expected ETag header" )
}
req2 := httptest . NewRequest ( http . MethodGet , "/api/config/token-list" , nil )
req2 . Header . Set ( "If-None-Match" , etag )
w2 := httptest . NewRecorder ( )
handler . ServeHTTP ( w2 , req2 )
if w2 . Code != http . StatusNotModified {
t . Fatalf ( "expected 304, got %d" , w2 . Code )
}
}
2026-03-28 13:40:32 -07:00
func TestConfigEndpointsSupportOptionsPreflight ( t * testing . T ) {
handler := setupConfigHandler ( )
req := httptest . NewRequest ( http . MethodOptions , "/api/config/token-list" , nil )
w := httptest . NewRecorder ( )
handler . ServeHTTP ( w , req )
if w . Code != http . StatusOK {
t . Fatalf ( "expected 200 preflight response, got %d" , w . Code )
}
if got := w . Header ( ) . Get ( "Access-Control-Allow-Methods" ) ; got == "" {
t . Fatal ( "expected Access-Control-Allow-Methods header" )
}
}