From a5d50fa59cf0beb871aa22822efe542f9327a987 Mon Sep 17 00:00:00 2001 From: Obsidian <131651958+0xObsidian@users.noreply.github.com> Date: Sat, 7 Dec 2024 00:06:56 -0500 Subject: [PATCH 1/2] feat: added env read support This commit introduces environment configuration support across the SDK and fixes an issue where client tests were being skipped. Description ----------- - Added scans for .env in current and parent directories - Added clean getters for all environment variables - Introduced graceful fallback to system environment variables - Added comprehensive test coverage for the introduced feature Testing ------- From the repo root, run: ``` go test -v ./config/env -run TestEnv ``` --- client/eth/client_test.go | 17 ++++--- client/eth/connection_pool_test.go | 34 ++++++------- config/env/env.go | 81 +++++++++++++++++++++++++++++- config/env/env_test.go | 60 ++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 6 files changed, 170 insertions(+), 25 deletions(-) create mode 100644 config/env/env_test.go diff --git a/client/eth/client_test.go b/client/eth/client_test.go index ab39233d..aa88c109 100644 --- a/client/eth/client_test.go +++ b/client/eth/client_test.go @@ -2,11 +2,11 @@ package eth_test import ( "context" - "os" "testing" "time" "github.com/berachain/offchain-sdk/client/eth" + "github.com/berachain/offchain-sdk/config/env" "github.com/stretchr/testify/assert" "github.com/ethereum/go-ethereum" @@ -29,6 +29,11 @@ const ( TestModeEither ) +func init() { + // Load environment variables before running tests + env.Load() +} + // NOTE: requires Ethereum chain rpc url at env var `ETH_RPC_URL` or `ETH_RPC_URL_WS`. func setUp(testMode int, t *testing.T) (*eth.ExtendedEthClient, error) { rpcTimeout := 5 * time.Second @@ -38,12 +43,12 @@ func setUp(testMode int, t *testing.T) (*eth.ExtendedEthClient, error) { var ethRPC string switch testMode { case TestModeWS: - ethRPC = os.Getenv("ETH_RPC_URL_WS") + ethRPC = env.GetEthWSURL() case TestModeHTTP: - ethRPC = os.Getenv("ETH_RPC_URL") + ethRPC = env.GetEthRPCURL() case TestModeEither: - if ethRPC = os.Getenv("ETH_RPC_URL_WS"); ethRPC == "" { - ethRPC = os.Getenv("ETH_RPC_URL") + if ethRPC = env.GetEthWSURL(); ethRPC == "" { + ethRPC = env.GetEthRPCURL() } default: panic("invalid test mode") @@ -130,7 +135,7 @@ func TestTxPoolContentFrom(t *testing.T) { assert.NoError(t, err) ctx := context.Background() - addrStr := os.Getenv("ETH_ADDR") + addrStr := env.GetAddressToListen() if addrStr == "" { t.Skipf("Skipping test: no eth address provided") } diff --git a/client/eth/connection_pool_test.go b/client/eth/connection_pool_test.go index 1921bea3..97777602 100644 --- a/client/eth/connection_pool_test.go +++ b/client/eth/connection_pool_test.go @@ -3,26 +3,24 @@ package eth_test import ( "bytes" "io" - "os" "testing" "github.com/berachain/offchain-sdk/client/eth" + "github.com/berachain/offchain-sdk/config/env" "github.com/berachain/offchain-sdk/log" "github.com/stretchr/testify/require" ) -var ( - HTTPURL = os.Getenv("ETH_HTTP_URL") - WSURL = os.Getenv("ETH_WS_URL") -) - -/******************************* HELPER FUNCTIONS ***************************************/ +func init() { + // Load environment variables before running tests + env.Load() +} -// NOTE: requires chain rpc url at env var `ETH_HTTP_URL` and `ETH_WS_URL`. +// NOTE: requires chain rpc url at env var `ETH_RPC_URL` and `ETH_WS_URL`. func checkEnv(t *testing.T) { - ethHTTPRPC := os.Getenv("ETH_HTTP_URL") - ethWSRPC := os.Getenv("ETH_WS_URL") - if ethHTTPRPC == "" || ethWSRPC == "" { + ethRPC := env.GetEthRPCURL() + ethWS := env.GetEthWSURL() + if ethRPC == "" || ethWS == "" { t.Skipf("Skipping test: no eth rpc url provided") } } @@ -58,7 +56,7 @@ func TestNewConnectionPoolImpl_MissingURLs(t *testing.T) { // TestNewConnectionPoolImpl_MissingWSURLs tests the case when the WS URLs are missing. func TestNewConnectionPoolImpl_MissingWSURLs(t *testing.T) { cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, } var logBuffer bytes.Buffer pool, err := Init(cfg, &logBuffer, t) @@ -72,8 +70,8 @@ func TestNewConnectionPoolImpl_MissingWSURLs(t *testing.T) { // It should the expected behavior. func TestNewConnectionPoolImpl(t *testing.T) { cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, - EthWSURLs: []string{WSURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, + EthWSURLs: []string{env.GetEthWSURL()}, } var logBuffer bytes.Buffer pool, err := Init(cfg, &logBuffer, t) @@ -87,7 +85,7 @@ func TestNewConnectionPoolImpl(t *testing.T) { // has been set and the connection has been established. func TestGetHTTP(t *testing.T) { cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, } var logBuffer bytes.Buffer pool, _ := Init(cfg, &logBuffer, t) @@ -103,8 +101,8 @@ func TestGetHTTP(t *testing.T) { // has been set and the connection has been established. func TestGetWS(t *testing.T) { cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, - EthWSURLs: []string{WSURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, + EthWSURLs: []string{env.GetEthWSURL()}, } var logBuffer bytes.Buffer pool, _ := Init(cfg, &logBuffer, t) @@ -121,7 +119,7 @@ func TestGetWS(t *testing.T) { // no WS URLs have been provided. func TestGetWS_WhenItIsNotSet(t *testing.T) { cfg := eth.ConnectionPoolConfig{ - EthHTTPURLs: []string{HTTPURL}, + EthHTTPURLs: []string{env.GetEthRPCURL()}, } var logBuffer bytes.Buffer pool, _ := Init(cfg, &logBuffer, t) diff --git a/config/env/env.go b/config/env/env.go index a10c6058..a08b433b 100644 --- a/config/env/env.go +++ b/config/env/env.go @@ -1,3 +1,82 @@ package env -// TODO support reading .env +import ( + "os" + "path/filepath" + + "github.com/joho/godotenv" +) + +const ( + // Ethereum RPC URLs + EnvEthRPCURL = "ETH_RPC_URL" + EnvEthWSURL = "ETH_WS_URL" + EnvEthRPCURLWS = "ETH_RPC_URL_WS" // Alternative WS URL used in some tests + + // Event listening configuration + EnvEventName = "EVENT_NAME" + EnvAddressListen = "ADDRESS_TO_LISTEN" +) + +// Loads environment variables from .env file +func Load() error { + // Try loading from current directory first + err := godotenv.Load() + if err == nil { + return nil + } + + // Then If that fails, try to find .env in + // parent directories + dir, err := os.Getwd() + if err != nil { + return err + } + + for { + envPath := filepath.Join(dir, ".env") + if _, err := os.Stat(envPath); err == nil { + return godotenv.Load(envPath) + } + + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + + // If we get here, we couldn't find the .env file + // But we don't return an error because the env vars + // might be actually set in the system in which case + // we don't need the .env file + return nil +} + +// Loads environment variables from the specified file +func LoadFile(filename string) error { + return godotenv.Load(filename) +} + +// Returns the Ethereum RPC URL +func GetEthRPCURL() string { + return os.Getenv(EnvEthRPCURL) +} + +// Returns the Ethereum WebSocket URL +func GetEthWSURL() string { + if url := os.Getenv(EnvEthRPCURLWS); url != "" { + return url + } + return os.Getenv(EnvEthWSURL) +} + +// Returns the event name to listen for +func GetEventName() string { + return os.Getenv(EnvEventName) +} + +// Returns the contract address to listen to +func GetAddressToListen() string { + return os.Getenv(EnvAddressListen) +} diff --git a/config/env/env_test.go b/config/env/env_test.go new file mode 100644 index 00000000..b6004844 --- /dev/null +++ b/config/env/env_test.go @@ -0,0 +1,60 @@ +package env + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEnv(t *testing.T) { + t.Run("test loading from .env file", func(t *testing.T) { + // Creating a temporary .env file for this test + dir := t.TempDir() + envFile := filepath.Join(dir, ".env") + err := os.WriteFile(envFile, []byte(` +ETH_RPC_URL=http://localhost:8545 +ETH_WS_URL=ws://localhost:8546 +ETH_RPC_URL_WS=ws://localhost:8547 +EVENT_NAME=NumberChanged(uint256) +ADDRESS_TO_LISTEN=0x5793a71D3eF074f71dCC21216Dbfd5C0e780132c +`), 0644) + require.NoError(t, err) + + // Loading the env file + err = LoadFile(envFile) + require.NoError(t, err) + + // Testing each getter + require.Equal(t, "http://localhost:8545", GetEthRPCURL()) + require.Equal(t, "ws://localhost:8547", GetEthWSURL(), "should prefer ETH_RPC_URL_WS") + + // Clearing ETH_RPC_URL_WS and verifying fallback to ETH_WS_URL + os.Unsetenv(EnvEthRPCURLWS) + require.Equal(t, "ws://localhost:8546", GetEthWSURL(), "should fallback to ETH_WS_URL") + + require.Equal(t, "NumberChanged(uint256)", GetEventName()) + require.Equal(t, "0x5793a71D3eF074f71dCC21216Dbfd5C0e780132c", GetAddressToListen()) + }) + + t.Run("test loading non-existent file", func(t *testing.T) { + err := LoadFile("non-existent.env") + require.Error(t, err) + }) + + t.Run("test loading with missing values", func(t *testing.T) { + // Clearing all env vars first + os.Unsetenv(EnvEthRPCURL) + os.Unsetenv(EnvEthWSURL) + os.Unsetenv(EnvEthRPCURLWS) + os.Unsetenv(EnvEventName) + os.Unsetenv(EnvAddressListen) + + // Testing empty values + require.Empty(t, GetEthRPCURL()) + require.Empty(t, GetEthWSURL()) + require.Empty(t, GetEventName()) + require.Empty(t, GetAddressToListen()) + }) +} diff --git a/go.mod b/go.mod index 1cbd11df..6160bb62 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/holiman/uint256 v1.2.4 github.com/huandu/skiplist v1.2.0 github.com/jellydator/ttlcache/v2 v2.11.1 + github.com/joho/godotenv v1.5.1 github.com/prometheus/client_golang v1.17.0 github.com/redis/go-redis/v9 v9.5.1 github.com/rs/zerolog v1.31.0 diff --git a/go.sum b/go.sum index b9444b13..788038a1 100644 --- a/go.sum +++ b/go.sum @@ -418,6 +418,8 @@ github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pk github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= From 3d5c5fb1c65556267a0e19eef3864d1abfd94cde Mon Sep 17 00:00:00 2001 From: Obsidian <131651958+0xObsidian@users.noreply.github.com> Date: Sat, 7 Dec 2024 01:14:18 -0500 Subject: [PATCH 2/2] refactor(tests): improve env setup per linting Description ----------- - Replaced init with setupClientTest/setupTest functions - Updated test calls with new setup functions Build verified with: ``` make lint && make format ``` Test coverage unchanged --- client/eth/client_test.go | 9 ++++++--- client/eth/connection_pool_test.go | 12 +++++++++--- config/env/env.go | 22 +++++++++++----------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/client/eth/client_test.go b/client/eth/client_test.go index aa88c109..7ecee930 100644 --- a/client/eth/client_test.go +++ b/client/eth/client_test.go @@ -29,13 +29,16 @@ const ( TestModeEither ) -func init() { - // Load environment variables before running tests - env.Load() +// setupClientTest loads environment variables and performs any necessary test setup. +func setupClientTest(t *testing.T) { + t.Helper() + err := env.Load() + assert.NoError(t, err) } // NOTE: requires Ethereum chain rpc url at env var `ETH_RPC_URL` or `ETH_RPC_URL_WS`. func setUp(testMode int, t *testing.T) (*eth.ExtendedEthClient, error) { + setupClientTest(t) rpcTimeout := 5 * time.Second ctxWithTimeout, cancel := context.WithTimeout(context.Background(), rpcTimeout) defer cancel() diff --git a/client/eth/connection_pool_test.go b/client/eth/connection_pool_test.go index 97777602..c64b3c98 100644 --- a/client/eth/connection_pool_test.go +++ b/client/eth/connection_pool_test.go @@ -11,9 +11,11 @@ import ( "github.com/stretchr/testify/require" ) -func init() { - // Load environment variables before running tests - env.Load() +// setupTest loads environment variables and performs any necessary test setup. +func setupTest(t *testing.T) { + t.Helper() + err := env.Load() + require.NoError(t, err) } // NOTE: requires chain rpc url at env var `ETH_RPC_URL` and `ETH_WS_URL`. @@ -69,6 +71,7 @@ func TestNewConnectionPoolImpl_MissingWSURLs(t *testing.T) { // TestNewConnectionPoolImpl tests the case when the URLs are provided. // It should the expected behavior. func TestNewConnectionPoolImpl(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ EthHTTPURLs: []string{env.GetEthRPCURL()}, EthWSURLs: []string{env.GetEthWSURL()}, @@ -84,6 +87,7 @@ func TestNewConnectionPoolImpl(t *testing.T) { // TestGetHTTP tests the retrieval of the HTTP client when it // has been set and the connection has been established. func TestGetHTTP(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ EthHTTPURLs: []string{env.GetEthRPCURL()}, } @@ -100,6 +104,7 @@ func TestGetHTTP(t *testing.T) { // TestGetWS tests the retrieval of the HTTP client when it // has been set and the connection has been established. func TestGetWS(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ EthHTTPURLs: []string{env.GetEthRPCURL()}, EthWSURLs: []string{env.GetEthWSURL()}, @@ -118,6 +123,7 @@ func TestGetWS(t *testing.T) { // TestGetWS_WhenItIsNotSet tests the retrieval of the WS client when // no WS URLs have been provided. func TestGetWS_WhenItIsNotSet(t *testing.T) { + setupTest(t) cfg := eth.ConnectionPoolConfig{ EthHTTPURLs: []string{env.GetEthRPCURL()}, } diff --git a/config/env/env.go b/config/env/env.go index a08b433b..75a61743 100644 --- a/config/env/env.go +++ b/config/env/env.go @@ -8,17 +8,17 @@ import ( ) const ( - // Ethereum RPC URLs + // Ethereum RPC URLs. EnvEthRPCURL = "ETH_RPC_URL" EnvEthWSURL = "ETH_WS_URL" EnvEthRPCURLWS = "ETH_RPC_URL_WS" // Alternative WS URL used in some tests - // Event listening configuration + // Event listening configuration. EnvEventName = "EVENT_NAME" EnvAddressListen = "ADDRESS_TO_LISTEN" ) -// Loads environment variables from .env file +// Loads environment variables from .env file. func Load() error { // Try loading from current directory first err := godotenv.Load() @@ -27,7 +27,7 @@ func Load() error { } // Then If that fails, try to find .env in - // parent directories + // parent directories. dir, err := os.Getwd() if err != nil { return err @@ -35,7 +35,7 @@ func Load() error { for { envPath := filepath.Join(dir, ".env") - if _, err := os.Stat(envPath); err == nil { + if _, statErr := os.Stat(envPath); statErr == nil { return godotenv.Load(envPath) } @@ -49,21 +49,21 @@ func Load() error { // If we get here, we couldn't find the .env file // But we don't return an error because the env vars // might be actually set in the system in which case - // we don't need the .env file + // we don't need the .env file. return nil } -// Loads environment variables from the specified file +// Loads environment variables from the specified file. func LoadFile(filename string) error { return godotenv.Load(filename) } -// Returns the Ethereum RPC URL +// Returns the Ethereum RPC URL. func GetEthRPCURL() string { return os.Getenv(EnvEthRPCURL) } -// Returns the Ethereum WebSocket URL +// Returns the Ethereum WebSocket URL. func GetEthWSURL() string { if url := os.Getenv(EnvEthRPCURLWS); url != "" { return url @@ -71,12 +71,12 @@ func GetEthWSURL() string { return os.Getenv(EnvEthWSURL) } -// Returns the event name to listen for +// Returns the event name to listen for. func GetEventName() string { return os.Getenv(EnvEventName) } -// Returns the contract address to listen to +// Returns the contract address to listen to. func GetAddressToListen() string { return os.Getenv(EnvAddressListen) }