Skip to content

Commit 789c24b

Browse files
committed
Changed the configuration loading and enhance API handlers
- Updated LoadConfig and LoadEnvironment functions to accept reader interfaces for improved flexibility. - Added new API routes for fetching the latest block height and the last X blocks. - Introduced new API routes for retrieving the last X amount of transactions. - Implemented unit tests for address, blocks, and transactions handlers. - Implemented unit tests for api config loader
1 parent a73fb3d commit 789c24b

15 files changed

Lines changed: 1001 additions & 41 deletions

api/config/loader.go

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,49 @@ import (
1010
"go.yaml.in/yaml/v4"
1111
)
1212

13-
func LoadConfig(path string) (*ApiConfig, error) {
14-
yamlFile, err := os.ReadFile(path)
13+
type FileReader interface {
14+
ReadFile(name string) ([]byte, error)
15+
}
16+
17+
type YamlFileReader struct{}
18+
19+
func (r *YamlFileReader) ReadFile(name string) ([]byte, error) {
20+
return os.ReadFile(name)
21+
}
22+
23+
type EnvFileReader interface {
24+
ReadFile(name string) error
25+
}
26+
27+
type DefaultEnvFileReader struct{}
28+
29+
func (r *DefaultEnvFileReader) ReadFile(name string) error {
30+
envPath := name
31+
fileInfo, err := os.Stat(name)
32+
if err == nil && fileInfo.IsDir() {
33+
envPath = filepath.Join(name, ".env")
34+
}
35+
36+
absPath, err := filepath.Abs(envPath)
37+
if err != nil {
38+
return err
39+
}
40+
41+
// Check for file existence first
42+
if _, err := os.Stat(absPath); err == nil {
43+
// File exists, load it
44+
err = godotenv.Load(absPath)
45+
if err != nil {
46+
return fmt.Errorf("error loading .env file: %w", err)
47+
}
48+
}
49+
// If file doesn't exist, that's OK - we'll use defaults or OS env vars
50+
51+
return nil
52+
}
53+
54+
func LoadConfig(reader FileReader, path string) (*ApiConfig, error) {
55+
yamlFile, err := reader.ReadFile(path)
1556
if err != nil {
1657
return nil, err
1758
}
@@ -32,33 +73,10 @@ func LoadConfig(path string) (*ApiConfig, error) {
3273
return &config, nil
3374
}
3475

35-
func LoadEnvironment(path string) (*ApiEnv, error) {
36-
possibleEnvFiles := []string{
37-
".env", // accept only .env for now
38-
}
39-
// check if any of the files exist within the current path
40-
existingFiles := []string{}
41-
for _, envFile := range possibleEnvFiles {
42-
formPath := filepath.Join(path, envFile)
43-
if _, err := os.Stat(formPath); err == nil {
44-
existingFiles = append(existingFiles, formPath)
45-
}
46-
}
47-
// if there are multiple files, decide which has highest priority
48-
// 1. production , 2. development, 3. local, 4. default
49-
// only use the regular .env for now return to this laster
50-
if len(existingFiles) == 0 {
51-
fmt.Println("No environment file found. Searching for os environment variables.")
52-
} else if len(existingFiles) == 1 {
53-
absPath, err := filepath.Abs(existingFiles[0])
54-
if err != nil {
55-
return nil, err
56-
}
57-
err = godotenv.Load(absPath)
58-
if err != nil {
59-
return nil, fmt.Errorf("error loading .env file: %w", err)
60-
}
61-
fmt.Printf("Loaded environment variables from %s\n", absPath)
76+
func LoadEnvironment(reader EnvFileReader, path string) (*ApiEnv, error) {
77+
err := reader.ReadFile(path)
78+
if err != nil {
79+
return nil, err
6280
}
6381

6482
environment := ApiEnv{}

api/config/loader_test.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package config_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/Cogwheel-Validator/spectra-gnoland-indexer/api/config"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// TestLoadConfig_ValidConfig tests loading a complete valid config
14+
func TestLoadConfig_ValidConfig(t *testing.T) {
15+
tmpDir := t.TempDir()
16+
17+
configContent := `host: 0.0.0.0
18+
port: 9000
19+
cors_allowed_origins:
20+
- "http://localhost:3000"
21+
- "http://localhost:8000"
22+
cors_allowed_methods:
23+
- "GET"
24+
- "POST"
25+
- "PUT"
26+
cors_allowed_headers:
27+
- "Content-Type"
28+
- "Authorization"
29+
cors_max_age: 7200
30+
chain_name: "gnoland"
31+
`
32+
33+
configPath := filepath.Join(tmpDir, "test-config.yml")
34+
err := os.WriteFile(configPath, []byte(configContent), 0644)
35+
require.NoError(t, err, "failed to write test config file")
36+
37+
cfg, err := config.LoadConfig(&config.YamlFileReader{}, configPath)
38+
require.NoError(t, err, "LoadConfig should not return an error")
39+
40+
assert.Equal(t, "0.0.0.0", cfg.Host)
41+
assert.Equal(t, 9000, cfg.Port)
42+
assert.Equal(t, "gnoland", cfg.ChainName)
43+
assert.Len(t, cfg.CorsAllowedOrigins, 2)
44+
assert.Equal(t, "http://localhost:3000", cfg.CorsAllowedOrigins[0])
45+
assert.Len(t, cfg.CorsAllowedMethods, 3)
46+
assert.Equal(t, "GET", cfg.CorsAllowedMethods[0])
47+
assert.Equal(t, 7200, cfg.CorsMaxAge)
48+
}
49+
50+
// TestLoadConfig_DefaultValues tests that empty fields get default values
51+
func TestLoadConfig_DefaultValues(t *testing.T) {
52+
tmpDir := t.TempDir()
53+
54+
// Minimal config with only required fields
55+
configContent := `chain_name: "gnoland"
56+
`
57+
58+
configPath := filepath.Join(tmpDir, "minimal-config.yml")
59+
err := os.WriteFile(configPath, []byte(configContent), 0644)
60+
require.NoError(t, err)
61+
62+
cfg, err := config.LoadConfig(&config.YamlFileReader{}, configPath)
63+
require.NoError(t, err)
64+
65+
// Verify defaults are applied
66+
assert.Equal(t, "localhost", cfg.Host)
67+
assert.Equal(t, 8080, cfg.Port)
68+
}
69+
70+
// TestLoadConfig_FileNotFound tests error handling when config file doesn't exist
71+
func TestLoadConfig_FileNotFound(t *testing.T) {
72+
_, err := config.LoadConfig(&config.YamlFileReader{}, "/nonexistent/path/to/config.yml")
73+
assert.Error(t, err, "should return error for missing file")
74+
}
75+
76+
// TestLoadConfig_InvalidYAML tests error handling for malformed YAML
77+
func TestLoadConfig_InvalidYAML(t *testing.T) {
78+
tmpDir := t.TempDir()
79+
80+
invalidYAML := `host: 0.0.0.0
81+
port: not_a_number: this_breaks_yaml
82+
chain_name: gnoland
83+
`
84+
85+
configPath := filepath.Join(tmpDir, "invalid-config.yml")
86+
err := os.WriteFile(configPath, []byte(invalidYAML), 0644)
87+
require.NoError(t, err)
88+
89+
_, err = config.LoadConfig(&config.YamlFileReader{}, configPath)
90+
assert.Error(t, err, "should return error for invalid YAML")
91+
}
92+
93+
// TestLoadConfig_EmptyFile tests handling of empty config file
94+
func TestLoadConfig_EmptyFile(t *testing.T) {
95+
tmpDir := t.TempDir()
96+
97+
configPath := filepath.Join(tmpDir, "empty-config.yml")
98+
err := os.WriteFile(configPath, []byte(""), 0644)
99+
require.NoError(t, err)
100+
101+
cfg, err := config.LoadConfig(&config.YamlFileReader{}, configPath)
102+
require.NoError(t, err, "should handle empty config gracefully")
103+
104+
// Should apply defaults
105+
assert.Equal(t, "localhost", cfg.Host)
106+
assert.Equal(t, 8080, cfg.Port)
107+
}
108+
109+
// TestLoadEnvironment_NoEnvFile tests that LoadEnvironment handles missing .env gracefully
110+
func TestLoadEnvironment_NoEnvFile(t *testing.T) {
111+
tmpDir := t.TempDir()
112+
113+
// Don't create any .env file
114+
// This should not error, just print a message to stdout
115+
env, err := config.LoadEnvironment(&config.DefaultEnvFileReader{}, tmpDir)
116+
require.NoError(t, err, "should not error when .env file doesn't exist")
117+
assert.NotNil(t, env)
118+
}
119+
120+
// TestLoadEnvironment_ValidEnvFile tests loading a valid .env file
121+
func TestLoadEnvironment_ValidEnvFile(t *testing.T) {
122+
tmpDir := t.TempDir()
123+
124+
envContent := `
125+
API_DB_HOST=postgres.example.com
126+
API_DB_PORT=5432
127+
API_DB_USER=testuser
128+
API_DB_PASSWORD=testpass
129+
API_DB_NAME=testdb
130+
`
131+
132+
envPath := filepath.Join(tmpDir, ".env")
133+
err := os.WriteFile(envPath, []byte(envContent), 0644)
134+
require.NoError(t, err)
135+
136+
// Save and clear any existing env vars to avoid interference
137+
oldHost := os.Getenv("API_DB_HOST")
138+
oldPort := os.Getenv("API_DB_PORT")
139+
oldUser := os.Getenv("API_DB_USER")
140+
oldPassword := os.Getenv("API_DB_PASSWORD")
141+
oldName := os.Getenv("API_DB_NAME")
142+
143+
err = os.Unsetenv("API_DB_HOST")
144+
require.NoError(t, err)
145+
err = os.Unsetenv("API_DB_PORT")
146+
require.NoError(t, err)
147+
err = os.Unsetenv("API_DB_USER")
148+
require.NoError(t, err)
149+
err = os.Unsetenv("API_DB_PASSWORD")
150+
require.NoError(t, err)
151+
err = os.Unsetenv("API_DB_NAME")
152+
require.NoError(t, err)
153+
154+
t.Cleanup(func() {
155+
// Restore original env vars
156+
if oldHost != "" {
157+
err = os.Setenv("API_DB_HOST", oldHost)
158+
require.NoError(t, err)
159+
}
160+
if oldPort != "" {
161+
err = os.Setenv("API_DB_PORT", oldPort)
162+
require.NoError(t, err)
163+
}
164+
if oldUser != "" {
165+
err = os.Setenv("API_DB_USER", oldUser)
166+
require.NoError(t, err)
167+
}
168+
if oldPassword != "" {
169+
err = os.Setenv("API_DB_PASSWORD", oldPassword)
170+
require.NoError(t, err)
171+
}
172+
if oldName != "" {
173+
err = os.Setenv("API_DB_NAME", oldName)
174+
require.NoError(t, err)
175+
}
176+
})
177+
178+
env, err := config.LoadEnvironment(&config.DefaultEnvFileReader{}, tmpDir)
179+
require.NoError(t, err)
180+
181+
assert.Equal(t, "postgres.example.com", env.ApiDbHost)
182+
assert.Equal(t, 5432, env.ApiDbPort)
183+
assert.Equal(t, "testuser", env.ApiDbUser)
184+
assert.Equal(t, "testpass", env.ApiDbPassword)
185+
assert.Equal(t, "testdb", env.ApiDbName)
186+
}
187+
188+
// TestLoadEnvironment_DefaultValues tests that defaults are applied for missing env vars
189+
func TestLoadEnvironment_DefaultValues(t *testing.T) {
190+
tmpDir := t.TempDir()
191+
192+
// Empty .env file will use all defaults
193+
envPath := filepath.Join(tmpDir, ".env")
194+
err := os.WriteFile(envPath, []byte(""), 0644)
195+
require.NoError(t, err)
196+
197+
// Save and clear any existing env vars to avoid interference
198+
oldHost := os.Getenv("API_DB_HOST")
199+
oldPort := os.Getenv("API_DB_PORT")
200+
oldUser := os.Getenv("API_DB_USER")
201+
oldPassword := os.Getenv("API_DB_PASSWORD")
202+
oldName := os.Getenv("API_DB_NAME")
203+
oldSslmode := os.Getenv("API_DB_SSLMODE")
204+
205+
err = os.Unsetenv("API_DB_HOST")
206+
require.NoError(t, err)
207+
err = os.Unsetenv("API_DB_PORT")
208+
require.NoError(t, err)
209+
err = os.Unsetenv("API_DB_USER")
210+
require.NoError(t, err)
211+
err = os.Unsetenv("API_DB_PASSWORD")
212+
require.NoError(t, err)
213+
err = os.Unsetenv("API_DB_NAME")
214+
require.NoError(t, err)
215+
err = os.Unsetenv("API_DB_SSLMODE")
216+
require.NoError(t, err)
217+
218+
t.Cleanup(func() {
219+
// Restore original env vars
220+
if oldHost != "" {
221+
err = os.Setenv("API_DB_HOST", oldHost)
222+
require.NoError(t, err)
223+
}
224+
if oldPort != "" {
225+
err = os.Setenv("API_DB_PORT", oldPort)
226+
require.NoError(t, err)
227+
}
228+
if oldUser != "" {
229+
err = os.Setenv("API_DB_USER", oldUser)
230+
require.NoError(t, err)
231+
}
232+
if oldPassword != "" {
233+
err = os.Setenv("API_DB_PASSWORD", oldPassword)
234+
require.NoError(t, err)
235+
}
236+
if oldName != "" {
237+
err = os.Setenv("API_DB_NAME", oldName)
238+
require.NoError(t, err)
239+
}
240+
if oldSslmode != "" {
241+
err = os.Setenv("API_DB_SSLMODE", oldSslmode)
242+
require.NoError(t, err)
243+
}
244+
})
245+
246+
env, err := config.LoadEnvironment(&config.DefaultEnvFileReader{}, tmpDir)
247+
require.NoError(t, err)
248+
249+
// Verify defaults from types.go
250+
assert.Equal(t, "localhost", env.ApiDbHost)
251+
assert.Equal(t, 5432, env.ApiDbPort)
252+
assert.Equal(t, "postgres", env.ApiDbUser)
253+
assert.Equal(t, "12345678", env.ApiDbPassword)
254+
assert.Equal(t, "gnoland", env.ApiDbName)
255+
assert.Equal(t, "disable", env.ApiDbSslmode)
256+
}

api/handlers/address.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ import (
44
"context"
55

66
humatypes "github.com/Cogwheel-Validator/spectra-gnoland-indexer/api/huma-types"
7-
"github.com/Cogwheel-Validator/spectra-gnoland-indexer/pkgs/database"
87
"github.com/danielgtaylor/huma/v2"
98
)
109

1110
type AddressHandler struct {
12-
db *database.TimescaleDb
11+
db DatabaseHandler
1312
chainName string
1413
}
1514

16-
func NewAddressHandler(db *database.TimescaleDb, chainName string) *AddressHandler {
15+
func NewAddressHandler(db DatabaseHandler, chainName string) *AddressHandler {
1716
return &AddressHandler{db: db, chainName: chainName}
1817
}
1918

0 commit comments

Comments
 (0)