diff --git a/assets/fs.go b/assets/fs.go new file mode 100644 index 0000000..8f77700 --- /dev/null +++ b/assets/fs.go @@ -0,0 +1,8 @@ +package assets + +import ( + "embed" +) + +//go:embed * +var EmbedFS embed.FS diff --git a/assets/invalid.toml b/assets/invalid.toml new file mode 100644 index 0000000..e4620eb --- /dev/null +++ b/assets/invalid.toml @@ -0,0 +1 @@ +notvalid \ No newline at end of file diff --git a/assets/valid.toml b/assets/valid.toml new file mode 100644 index 0000000..76a8a63 --- /dev/null +++ b/assets/valid.toml @@ -0,0 +1,10 @@ +[[chains]] +name = "sentinel" +lcd-endpoint = "https://api.sentinel.quokkastake.io" +coingecko-currency = "sentinel" +base-denom = "udvpn" +denom = "dvpn" +bech-wallet-prefix = "sent" +validators = [ + { address = "sentvaloper1rw9wtyhsus7jvx55v3qv5nzun054ma6kz4237k" } +] diff --git a/cmd/cosmos-validators-exporter.go b/cmd/cosmos-validators-exporter.go index 91ef95e..722e232 100644 --- a/cmd/cosmos-validators-exporter.go +++ b/cmd/cosmos-validators-exporter.go @@ -4,6 +4,7 @@ import ( "main/pkg" configPkg "main/pkg/config" "main/pkg/logger" + "os" "github.com/spf13/cobra" ) @@ -12,13 +13,22 @@ var ( version = "unknown" ) +type OsFS struct { +} + +func (fs *OsFS) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} + func ExecuteMain(configPath string) { - app := pkg.NewApp(configPath, version) + filesystem := &OsFS{} + app := pkg.NewApp(configPath, filesystem, version) app.Start() } func ExecuteValidateConfig(configPath string) { - config, err := configPkg.GetConfig(configPath) + filesystem := &OsFS{} + config, err := configPkg.GetConfig(configPath, filesystem) if err != nil { logger.GetDefaultLogger().Fatal().Err(err).Msg("Could not load config!") } @@ -27,7 +37,17 @@ func ExecuteValidateConfig(configPath string) { logger.GetDefaultLogger().Fatal().Err(err).Msg("Config is invalid!") } - config.DisplayWarnings(logger.GetDefaultLogger()) + warnings := config.DisplayWarnings() + + for _, warning := range warnings { + entry := logger.GetDefaultLogger().Warn() + for label, value := range warning.Labels { + entry = entry.Str(label, value) + } + + entry.Msg(warning.Message) + } + logger.GetDefaultLogger().Info().Msg("Provided config is valid.") } diff --git a/pkg/app.go b/pkg/app.go index 35e84d3..258e90b 100644 --- a/pkg/app.go +++ b/pkg/app.go @@ -2,6 +2,7 @@ package pkg import ( fetchersPkg "main/pkg/fetchers" + "main/pkg/fs" generatorsPkg "main/pkg/generators" coingeckoPkg "main/pkg/price_fetchers/coingecko" dexScreenerPkg "main/pkg/price_fetchers/dex_screener" @@ -45,8 +46,8 @@ type App struct { Generators []generatorsPkg.Generator } -func NewApp(configPath string, version string) *App { - appConfig, err := config.GetConfig(configPath) +func NewApp(configPath string, filesystem fs.FS, version string) *App { + appConfig, err := config.GetConfig(configPath, filesystem) if err != nil { loggerPkg.GetDefaultLogger().Fatal().Err(err).Msg("Could not load config") } @@ -56,7 +57,15 @@ func NewApp(configPath string, version string) *App { } logger := loggerPkg.GetLogger(appConfig.LogConfig) - appConfig.DisplayWarnings(logger) + warnings := appConfig.DisplayWarnings() + for _, warning := range warnings { + entry := logger.Warn() + for label, value := range warning.Labels { + entry = entry.Str(label, value) + } + + entry.Msg(warning.Message) + } tracer, err := tracing.InitTracer(appConfig.TracingConfig, version) if err != nil { diff --git a/pkg/config/chain.go b/pkg/config/chain.go index 4e4bb94..91f916a 100644 --- a/pkg/config/chain.go +++ b/pkg/config/chain.go @@ -5,8 +5,6 @@ import ( "fmt" "github.com/guregu/null/v5" - - "github.com/rs/zerolog" ) type Chain struct { @@ -68,14 +66,23 @@ func (c *Chain) Validate() error { return nil } -func (c *Chain) DisplayWarnings(logger *zerolog.Logger) { +func (c *Chain) DisplayWarnings() []Warning { + warnings := []Warning{} + if c.BaseDenom == "" { - logger.Warn(). - Str("chain", c.Name). - Msg("Base denom is not set") + warnings = append(warnings, Warning{ + Message: "Base denom is not set", + Labels: map[string]string{"chain": c.Name}, + }) } for _, denom := range c.Denoms { - denom.DisplayWarnings(c, logger) + warnings = append(warnings, denom.DisplayWarnings(c)...) } + + for _, validator := range c.Validators { + warnings = append(warnings, validator.DisplayWarnings(c)...) + } + + return warnings } diff --git a/pkg/config/chain_test.go b/pkg/config/chain_test.go new file mode 100644 index 0000000..12bc439 --- /dev/null +++ b/pkg/config/chain_test.go @@ -0,0 +1,139 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChainMethods(t *testing.T) { + t.Parallel() + + chain := Chain{ + LCDEndpoint: "example", + Name: "chain", + Queries: map[string]bool{"enabled": true}, + } + + assert.Equal(t, "example", chain.GetHost()) + assert.Equal(t, "chain", chain.GetName()) + assert.Len(t, chain.GetQueries(), 1) +} + +func TestChainValidateNoName(t *testing.T) { + t.Parallel() + + chain := Chain{} + err := chain.Validate() + require.Error(t, err) +} + +func TestChainValidateNoEndpoint(t *testing.T) { + t.Parallel() + + chain := Chain{Name: "test"} + err := chain.Validate() + require.Error(t, err) +} + +func TestChainValidateNoValidators(t *testing.T) { + t.Parallel() + + chain := Chain{Name: "test", LCDEndpoint: "test"} + err := chain.Validate() + require.Error(t, err) +} + +func TestChainValidateInvalidValidator(t *testing.T) { + t.Parallel() + + chain := Chain{ + Name: "test", + LCDEndpoint: "test", + Validators: []Validator{{}}, + } + err := chain.Validate() + require.Error(t, err) +} + +func TestChainValidateInvalidDenom(t *testing.T) { + t.Parallel() + + chain := Chain{ + Name: "test", + LCDEndpoint: "test", + Validators: []Validator{{Address: "test"}}, + Denoms: DenomInfos{{}}, + } + err := chain.Validate() + require.Error(t, err) +} + +func TestChainValidateInvalidConsumer(t *testing.T) { + t.Parallel() + + chain := Chain{ + Name: "test", + LCDEndpoint: "test", + Validators: []Validator{{Address: "test"}}, + Denoms: DenomInfos{{Denom: "ustake", DisplayDenom: "stake"}}, + ConsumerChains: []*ConsumerChain{{}}, + } + err := chain.Validate() + require.Error(t, err) +} + +func TestChainValidateValid(t *testing.T) { + t.Parallel() + + chain := Chain{ + Name: "test", + LCDEndpoint: "test", + Validators: []Validator{{Address: "test"}}, + Denoms: DenomInfos{{Denom: "ustake", DisplayDenom: "stake"}}, + } + err := chain.Validate() + require.NoError(t, err) +} + +func TestChainDisplayWarningsNoBaseDenom(t *testing.T) { + t.Parallel() + + chain := Chain{ + Name: "test", + LCDEndpoint: "test", + Validators: []Validator{{Address: "test"}}, + Denoms: DenomInfos{{Denom: "ustake", DisplayDenom: "stake", CoingeckoCurrency: "stake"}}, + } + warnings := chain.DisplayWarnings() + require.NotEmpty(t, warnings) +} + +func TestChainDisplayWarningsDenomWarning(t *testing.T) { + t.Parallel() + + chain := Chain{ + Name: "test", + LCDEndpoint: "test", + BaseDenom: "ustake", + Validators: []Validator{{Address: "test", ConsensusAddress: "test"}}, + Denoms: DenomInfos{{Denom: "ustake", DisplayDenom: "stake"}}, + } + warnings := chain.DisplayWarnings() + require.NotEmpty(t, warnings) +} + +func TestChainDisplayWarningsEmpty(t *testing.T) { + t.Parallel() + + chain := Chain{ + Name: "test", + LCDEndpoint: "test", + BaseDenom: "ustake", + Validators: []Validator{{Address: "test", ConsensusAddress: "test"}}, + Denoms: DenomInfos{{Denom: "ustake", DisplayDenom: "stake", CoingeckoCurrency: "stake"}}, + } + warnings := chain.DisplayWarnings() + require.Empty(t, warnings) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 6cc7c62..4f371f5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,27 +3,12 @@ package config import ( "errors" "fmt" - "os" - - "github.com/rs/zerolog" + "main/pkg/fs" "github.com/BurntSushi/toml" "github.com/creasty/defaults" ) -type Validator struct { - Address string `toml:"address"` - ConsensusAddress string `toml:"consensus-address"` -} - -func (v *Validator) Validate() error { - if v.Address == "" { - return errors.New("validator address is expected!") - } - - return nil -} - type Config struct { LogConfig LogConfig `toml:"log"` TracingConfig TracingConfig `toml:"tracing"` @@ -55,10 +40,14 @@ func (c *Config) Validate() error { return nil } -func (c *Config) DisplayWarnings(logger *zerolog.Logger) { +func (c *Config) DisplayWarnings() []Warning { + warnings := []Warning{} + for _, chain := range c.Chains { - chain.DisplayWarnings(logger) + warnings = append(warnings, chain.DisplayWarnings()...) } + + return warnings } func (c *Config) GetCoingeckoCurrencies() []string { @@ -70,13 +59,21 @@ func (c *Config) GetCoingeckoCurrencies() []string { currencies = append(currencies, denom.CoingeckoCurrency) } } + + for _, consumerChain := range chain.ConsumerChains { + for _, denom := range consumerChain.Denoms { + if denom.CoingeckoCurrency != "" { + currencies = append(currencies, denom.CoingeckoCurrency) + } + } + } } return currencies } -func GetConfig(path string) (*Config, error) { - configBytes, err := os.ReadFile(path) +func GetConfig(path string, filesystem fs.FS) (*Config, error) { + configBytes, err := filesystem.ReadFile(path) if err != nil { return nil, err } @@ -88,9 +85,6 @@ func GetConfig(path string) (*Config, error) { return nil, err } - if err = defaults.Set(&configStruct); err != nil { - return nil, err - } - + defaults.MustSet(&configStruct) return &configStruct, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..db3f5be --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,135 @@ +package config + +import ( + "main/pkg/fs" + "testing" + + "github.com/guregu/null/v5" + "github.com/stretchr/testify/require" +) + +func TestConfigValidateInvalidTracingConfig(t *testing.T) { + t.Parallel() + + config := Config{ + TracingConfig: TracingConfig{Enabled: null.BoolFrom(true)}, + } + + err := config.Validate() + require.Error(t, err) +} + +func TestConfigValidateNoChains(t *testing.T) { + t.Parallel() + + config := Config{} + + err := config.Validate() + require.Error(t, err) +} + +func TestConfigValidateInvalidChain(t *testing.T) { + t.Parallel() + + config := Config{ + Chains: []*Chain{{}}, + } + + err := config.Validate() + require.Error(t, err) +} + +func TestConfigValidateValid(t *testing.T) { + t.Parallel() + + config := Config{ + Chains: []*Chain{{ + Name: "chain", + LCDEndpoint: "test", + Validators: []Validator{{Address: "test"}}, + }}, + } + + err := config.Validate() + require.NoError(t, err) +} + +func TestDisplayWarningsChainWarning(t *testing.T) { + t.Parallel() + + config := Config{ + Chains: []*Chain{{ + Name: "chain", + LCDEndpoint: "test", + Validators: []Validator{{Address: "test"}}, + }}, + } + + warnings := config.DisplayWarnings() + require.NotEmpty(t, warnings) +} + +func TestDisplayWarningsEmpty(t *testing.T) { + t.Parallel() + + config := Config{ + Chains: []*Chain{{ + Name: "chain", + LCDEndpoint: "test", + BaseDenom: "test", + Validators: []Validator{{Address: "test", ConsensusAddress: "test"}}, + }}, + } + + warnings := config.DisplayWarnings() + require.Empty(t, warnings) +} + +func TestCoingeckoCurrencies(t *testing.T) { + t.Parallel() + + config := Config{ + Chains: []*Chain{{ + Denoms: DenomInfos{ + {Denom: "denom1", CoingeckoCurrency: "denom1"}, + {Denom: "denom2"}, + }, + ConsumerChains: []*ConsumerChain{{ + Denoms: DenomInfos{ + {Denom: "denom3", CoingeckoCurrency: "denom3"}, + {Denom: "denom4"}, + }, + }}, + }}, + } + + currencies := config.GetCoingeckoCurrencies() + require.Len(t, currencies, 2) + require.Contains(t, currencies, "denom1") + require.Contains(t, currencies, "denom3") +} + +func TestLoadConfigNotFound(t *testing.T) { + t.Parallel() + + filesystem := &fs.TestFS{} + _, err := GetConfig("not-existing.toml", filesystem) + require.Error(t, err) +} + +func TestLoadConfigInvalid(t *testing.T) { + t.Parallel() + + filesystem := &fs.TestFS{} + _, err := GetConfig("invalid.toml", filesystem) + require.Error(t, err) +} + +func TestLoadConfigValid(t *testing.T) { + t.Parallel() + + filesystem := &fs.TestFS{} + config, err := GetConfig("valid.toml", filesystem) + require.NoError(t, err) + require.NotNil(t, config) +} diff --git a/pkg/config/consumer_chain_test.go b/pkg/config/consumer_chain_test.go new file mode 100644 index 0000000..c2725c4 --- /dev/null +++ b/pkg/config/consumer_chain_test.go @@ -0,0 +1,72 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConsumerChainMethods(t *testing.T) { + t.Parallel() + + chain := ConsumerChain{ + LCDEndpoint: "example", + Name: "chain", + Queries: map[string]bool{"enabled": true}, + } + + assert.Equal(t, "example", chain.GetHost()) + assert.Equal(t, "chain", chain.GetName()) + assert.Len(t, chain.GetQueries(), 1) +} + +func TestConsumerChainValidateNoName(t *testing.T) { + t.Parallel() + + chain := ConsumerChain{} + err := chain.Validate() + require.Error(t, err) +} + +func TestConsumerChainValidateNoEndpoint(t *testing.T) { + t.Parallel() + + chain := ConsumerChain{Name: "test"} + err := chain.Validate() + require.Error(t, err) +} + +func TestConsumerChainValidateNoChainId(t *testing.T) { + t.Parallel() + + chain := ConsumerChain{Name: "test", LCDEndpoint: "test"} + err := chain.Validate() + require.Error(t, err) +} + +func TestConsumerChainValidateInvalidDenom(t *testing.T) { + t.Parallel() + + chain := ConsumerChain{ + Name: "test", + LCDEndpoint: "test", + ChainID: "test", + Denoms: DenomInfos{{}}, + } + err := chain.Validate() + require.Error(t, err) +} + +func TestConsumerChainValidateValid(t *testing.T) { + t.Parallel() + + chain := ConsumerChain{ + Name: "test", + LCDEndpoint: "test", + ChainID: "test", + Denoms: DenomInfos{{Denom: "ustake", DisplayDenom: "stake"}}, + } + err := chain.Validate() + require.NoError(t, err) +} diff --git a/pkg/config/denom_info.go b/pkg/config/denom_info.go index 6be2c2c..b463a9c 100644 --- a/pkg/config/denom_info.go +++ b/pkg/config/denom_info.go @@ -2,8 +2,6 @@ package config import ( "errors" - - "github.com/rs/zerolog" ) type DenomInfo struct { @@ -27,13 +25,19 @@ func (d *DenomInfo) Validate() error { return nil } -func (d *DenomInfo) DisplayWarnings(chain *Chain, logger *zerolog.Logger) { +func (d *DenomInfo) DisplayWarnings(chain *Chain) []Warning { + warnings := []Warning{} if d.CoingeckoCurrency == "" && (d.DexScreenerPair == "" || d.DexScreenerChainID == "") { - logger.Warn(). - Str("chain", chain.Name). - Str("denom", d.Denom). - Msg("Currency code not set, not fetching exchange rate.") + warnings = append(warnings, Warning{ + Message: "Currency code not set, not fetching exchange rate.", + Labels: map[string]string{ + "chain": chain.Name, + "denom": d.Denom, + }, + }) } + + return warnings } type DenomInfos []*DenomInfo diff --git a/pkg/config/denom_info_test.go b/pkg/config/denom_info_test.go new file mode 100644 index 0000000..f755eec --- /dev/null +++ b/pkg/config/denom_info_test.go @@ -0,0 +1,57 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func TestDenomInfoValidateNoDenom(t *testing.T) { + t.Parallel() + + denom := DenomInfo{} + err := denom.Validate() + require.Error(t, err) +} + +func TestDenomInfoValidateNoDisplayDenom(t *testing.T) { + t.Parallel() + + denom := DenomInfo{Denom: "ustake"} + err := denom.Validate() + require.Error(t, err) +} + +func TestDenomInfoValidateValid(t *testing.T) { + t.Parallel() + + denom := DenomInfo{Denom: "ustake", DisplayDenom: "stake"} + err := denom.Validate() + require.NoError(t, err) +} + +func TestDenomInfoDisplayWarningNoCoingecko(t *testing.T) { + t.Parallel() + + denom := DenomInfo{Denom: "ustake", DisplayDenom: "stake"} + warnings := denom.DisplayWarnings(&Chain{Name: "test"}) + assert.NotEmpty(t, warnings) +} + +func TestDenomInfoDisplayWarningEmpty(t *testing.T) { + t.Parallel() + + denom := DenomInfo{Denom: "ustake", DisplayDenom: "stake", CoingeckoCurrency: "test"} + warnings := denom.DisplayWarnings(&Chain{Name: "test"}) + assert.Empty(t, warnings) +} + +func TestDenomInfosFind(t *testing.T) { + t.Parallel() + + denoms := DenomInfos{{Denom: "ustake"}} + assert.NotNil(t, denoms.Find("ustake")) + assert.Nil(t, denoms.Find("uatom")) +} diff --git a/pkg/config/display_warning.go b/pkg/config/display_warning.go new file mode 100644 index 0000000..8681d59 --- /dev/null +++ b/pkg/config/display_warning.go @@ -0,0 +1,6 @@ +package config + +type Warning struct { + Message string + Labels map[string]string +} diff --git a/pkg/config/query_test.go b/pkg/config/query_test.go new file mode 100644 index 0000000..0375612 --- /dev/null +++ b/pkg/config/query_test.go @@ -0,0 +1,16 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChainQueryEnabled(t *testing.T) { + t.Parallel() + + queries := Queries{"query1": true, "query2": false} + assert.True(t, queries.Enabled("query1")) + assert.False(t, queries.Enabled("query2")) + assert.True(t, queries.Enabled("query3")) +} diff --git a/pkg/config/tracing_test.go b/pkg/config/tracing_test.go new file mode 100644 index 0000000..4a22b7d --- /dev/null +++ b/pkg/config/tracing_test.go @@ -0,0 +1,25 @@ +package config + +import ( + "testing" + + "github.com/guregu/null/v5" + + "github.com/stretchr/testify/assert" +) + +func TestTracingInvalid(t *testing.T) { + t.Parallel() + + tracing := TracingConfig{Enabled: null.BoolFrom(true)} + err := tracing.Validate() + assert.Error(t, err) +} + +func TestTracingValid(t *testing.T) { + t.Parallel() + + tracing := TracingConfig{Enabled: null.BoolFrom(true), OpenTelemetryHTTPHost: "test"} + err := tracing.Validate() + assert.NoError(t, err) +} diff --git a/pkg/config/validator.go b/pkg/config/validator.go new file mode 100644 index 0000000..fa02711 --- /dev/null +++ b/pkg/config/validator.go @@ -0,0 +1,31 @@ +package config + +import "errors" + +type Validator struct { + Address string `toml:"address"` + ConsensusAddress string `toml:"consensus-address"` +} + +func (v *Validator) Validate() error { + if v.Address == "" { + return errors.New("validator address is expected!") + } + + return nil +} + +func (v *Validator) DisplayWarnings(chain *Chain) []Warning { + warnings := []Warning{} + if v.ConsensusAddress == "" { + warnings = append(warnings, Warning{ + Message: "Consensus address is not set, cannot display signing info metrics.", + Labels: map[string]string{ + "chain": chain.Name, + "validator": v.Address, + }, + }) + } + + return warnings +} diff --git a/pkg/config/validator_test.go b/pkg/config/validator_test.go new file mode 100644 index 0000000..b6549b1 --- /dev/null +++ b/pkg/config/validator_test.go @@ -0,0 +1,23 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidatorDisplayWarningNoConsensusAddress(t *testing.T) { + t.Parallel() + + validator := Validator{Address: "test"} + warnings := validator.DisplayWarnings(&Chain{Name: "test"}) + assert.NotEmpty(t, warnings) +} + +func TestValidatorDisplayWarningEmpty(t *testing.T) { + t.Parallel() + + validator := Validator{Address: "test", ConsensusAddress: "test"} + warnings := validator.DisplayWarnings(&Chain{Name: "test"}) + assert.Empty(t, warnings) +} diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go new file mode 100644 index 0000000..cf57e9e --- /dev/null +++ b/pkg/fs/fs.go @@ -0,0 +1,5 @@ +package fs + +type FS interface { + ReadFile(name string) ([]byte, error) +} diff --git a/pkg/fs/test_fs.go b/pkg/fs/test_fs.go new file mode 100644 index 0000000..b9b7644 --- /dev/null +++ b/pkg/fs/test_fs.go @@ -0,0 +1,11 @@ +package fs + +import ( + "main/assets" +) + +type TestFS struct{} + +func (fs *TestFS) ReadFile(name string) ([]byte, error) { + return assets.EmbedFS.ReadFile(name) +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 4d04420..a349bae 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -22,7 +22,7 @@ func GetLogger(appConfig config.LogConfig) *zerolog.Logger { logLevel, err := zerolog.ParseLevel(appConfig.LogLevel) if err != nil { - log.Fatal().Err(err).Msg("Could not parse log level") + log.Panic().Err(err).Msg("Could not parse log level") } zerolog.SetGlobalLevel(logLevel) diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go new file mode 100644 index 0000000..183f571 --- /dev/null +++ b/pkg/logger/logger_test.go @@ -0,0 +1,42 @@ +package logger_test + +import ( + "main/pkg/config" + loggerPkg "main/pkg/logger" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetDefaultLogger(t *testing.T) { + t.Parallel() + + logger := loggerPkg.GetDefaultLogger() + require.NotNil(t, logger) +} + +func TestGetLoggerInvalidLogLevel(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + require.Fail(t, "Expected to have a panic here!") + } + }() + + loggerPkg.GetLogger(config.LogConfig{LogLevel: "invalid"}) +} + +func TestGetLoggerValidPlain(t *testing.T) { + t.Parallel() + + logger := loggerPkg.GetLogger(config.LogConfig{LogLevel: "info"}) + require.NotNil(t, logger) +} + +func TestGetLoggerValidJSON(t *testing.T) { + t.Parallel() + + logger := loggerPkg.GetLogger(config.LogConfig{LogLevel: "info", JSONOutput: true}) + require.NotNil(t, logger) +} diff --git a/pkg/tracing/noop_exporter_test.go b/pkg/tracing/noop_exporter_test.go new file mode 100644 index 0000000..e99473d --- /dev/null +++ b/pkg/tracing/noop_exporter_test.go @@ -0,0 +1,37 @@ +package tracing + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + tracesdk "go.opentelemetry.io/otel/sdk/trace" +) + +func TestNoopExporterShutdown(t *testing.T) { + t.Parallel() + + exporter := NewNoopExporter() + err := exporter.Shutdown(context.Background()) + + require.NoError(t, err) +} + +func TestNoopExporterExportSpans(t *testing.T) { + t.Parallel() + + exporter := NewNoopExporter() + tp := NewTraceProvider(exporter, "1.2.3") + + _, span := tp.Tracer("test").Start(context.Background(), "test") + defer span.End() + + readOnlySpan, ok := span.(tracesdk.ReadOnlySpan) + assert.True(t, ok) + + err := exporter.ExportSpans(context.Background(), []tracesdk.ReadOnlySpan{readOnlySpan}) + + require.NoError(t, err) +} diff --git a/pkg/tracing/provider.go b/pkg/tracing/provider.go index 2f20214..90e168d 100644 --- a/pkg/tracing/provider.go +++ b/pkg/tracing/provider.go @@ -6,9 +6,9 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) -func NewTraceProvider(exp tracesdk.SpanExporter, version string) (*tracesdk.TracerProvider, error) { +func NewTraceProvider(exp tracesdk.SpanExporter, version string) *tracesdk.TracerProvider { // Ensure default SDK resources and the required service name are set. - r, err := resource.Merge( + r, _ := resource.Merge( resource.Default(), resource.NewWithAttributes( semconv.SchemaURL, @@ -16,12 +16,9 @@ func NewTraceProvider(exp tracesdk.SpanExporter, version string) (*tracesdk.Trac semconv.ServiceVersionKey.String(version), ), ) - if err != nil { - return nil, err - } return tracesdk.NewTracerProvider( tracesdk.WithBatcher(exp), tracesdk.WithResource(r), - ), nil + ) } diff --git a/pkg/tracing/tracer.go b/pkg/tracing/tracer.go index a301c61..fc2a20a 100644 --- a/pkg/tracing/tracer.go +++ b/pkg/tracing/tracer.go @@ -45,12 +45,15 @@ func InitTracer(config configPkg.TracingConfig, version string) (trace.Tracer, e return nil, fmt.Errorf("error creating exporter: %w", err) } - tp, err := NewTraceProvider(exporter, version) - if err != nil { - return nil, fmt.Errorf("error initializizng provider: %w", err) - } - + tp := NewTraceProvider(exporter, version) otel.SetTracerProvider(tp) return tp.Tracer("main"), nil } + +func InitNoopTracer() trace.Tracer { + tp := NewTraceProvider(NewNoopExporter(), "1.2.3") + otel.SetTracerProvider(tp) + + return tp.Tracer("main") +} diff --git a/pkg/tracing/tracer_test.go b/pkg/tracing/tracer_test.go new file mode 100644 index 0000000..b201cbe --- /dev/null +++ b/pkg/tracing/tracer_test.go @@ -0,0 +1,68 @@ +package tracing + +import ( + configPkg "main/pkg/config" + "testing" + + "github.com/guregu/null/v5" + "github.com/stretchr/testify/require" +) + +func TestTracerGetExporterNoop(t *testing.T) { + t.Parallel() + + config := configPkg.TracingConfig{} + exporter, err := getExporter(config) + + require.NoError(t, err) + require.NotNil(t, exporter) +} + +func TestTracerGetExporterHttpBasic(t *testing.T) { + t.Parallel() + + config := configPkg.TracingConfig{Enabled: null.BoolFrom(true)} + exporter, err := getExporter(config) + + require.NoError(t, err) + require.NotNil(t, exporter) +} + +func TestTracerGetExporterHttpComplex(t *testing.T) { + t.Parallel() + + config := configPkg.TracingConfig{ + Enabled: null.BoolFrom(true), + OpenTelemetryHTTPHost: "test", + OpenTelemetryHTTPUser: "test", + OpenTelemetryHTTPPassword: "test", + OpenTelemetryHTTPInsecure: null.BoolFrom(true), + } + exporter, err := getExporter(config) + + require.NoError(t, err) + require.NotNil(t, exporter) +} + +func TestTracerGetTracerOk(t *testing.T) { + t.Parallel() + + config := configPkg.TracingConfig{ + Enabled: null.BoolFrom(true), + OpenTelemetryHTTPHost: "test", + OpenTelemetryHTTPUser: "test", + OpenTelemetryHTTPPassword: "test", + OpenTelemetryHTTPInsecure: null.BoolFrom(true), + } + tracer, err := InitTracer(config, "v1.2.3") + + require.NoError(t, err) + require.NotNil(t, tracer) +} + +func TestTracerGetNoopTracerOk(t *testing.T) { + t.Parallel() + + tracer := InitNoopTracer() + require.NotNil(t, tracer) +} diff --git a/pkg/types/http_predicate_test.go b/pkg/types/http_predicate_test.go new file mode 100644 index 0000000..dd6ad1f --- /dev/null +++ b/pkg/types/http_predicate_test.go @@ -0,0 +1,52 @@ +package types + +import ( + "main/pkg/constants" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHTTPPredicateAlwaysPass(t *testing.T) { + t.Parallel() + + predicate := HTTPPredicateAlwaysPass() + require.NoError(t, predicate(&http.Response{})) +} + +func TestHTTPPredicateCheckHeightAfterErrorParsing(t *testing.T) { + t.Parallel() + + predicate := HTTPPredicateCheckHeightAfter(1) + + header := http.Header{ + constants.HeaderBlockHeight: []string{"invalid"}, + } + request := &http.Response{Header: header} + require.Error(t, predicate(request)) +} + +func TestHTTPPredicateCheckHeightAfterOlderBlock(t *testing.T) { + t.Parallel() + + predicate := HTTPPredicateCheckHeightAfter(100) + + header := http.Header{ + constants.HeaderBlockHeight: []string{"1"}, + } + request := &http.Response{Header: header} + require.Error(t, predicate(request)) +} + +func TestHTTPPredicateCheckHeightPass(t *testing.T) { + t.Parallel() + + predicate := HTTPPredicateCheckHeightAfter(100) + + header := http.Header{ + constants.HeaderBlockHeight: []string{"200"}, + } + request := &http.Response{Header: header} + require.NoError(t, predicate(request)) +} diff --git a/pkg/types/tendermint_test.go b/pkg/types/tendermint_test.go new file mode 100644 index 0000000..fa3399f --- /dev/null +++ b/pkg/types/tendermint_test.go @@ -0,0 +1,28 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidatorActive(t *testing.T) { + t.Parallel() + + assert.False(t, Validator{Status: "BOND_STATUS_UNBONDED"}.Active()) + assert.True(t, Validator{Status: "BOND_STATUS_BONDED"}.Active()) +} + +func TestResponseAmountToAmount(t *testing.T) { + t.Parallel() + + responseAmount := ResponseAmount{ + Amount: "1.23", + Denom: "ustake", + } + + converted := responseAmount.ToAmount() + + assert.InDelta(t, 1.23, 0.0001, converted.Amount) + assert.Equal(t, "ustake", converted.Denom) +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go new file mode 100644 index 0000000..571e29e --- /dev/null +++ b/pkg/utils/utils_test.go @@ -0,0 +1,199 @@ +package utils + +import ( + "main/pkg/constants" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type TestCompareStruct struct { + value int +} + +func TestCompareTwoBech32FirstInvalid(t *testing.T) { + t.Parallel() + + _, err := CompareTwoBech32("test", "cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2") + require.Error(t, err, "Error should be present!") +} + +func TestCompareTwoBech32SecondInvalid(t *testing.T) { + t.Parallel() + + _, err := CompareTwoBech32("cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2", "test") + require.Error(t, err, "Error should be present!") +} + +func TestCompareTwoBech32SecondEqual(t *testing.T) { + t.Parallel() + + equal, err := CompareTwoBech32( + "cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2", + "cosmosvaloper1xqz9pemz5e5zycaa89kys5aw6m8rhgsvw4328e", + ) + require.NoError(t, err, "Error should not be present!") + assert.True(t, equal, "Bech addresses should be equal!") +} + +func TestCompareTwoBech32SecondNotEqual(t *testing.T) { + t.Parallel() + + equal, err := CompareTwoBech32( + "cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2", + "cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q", + ) + require.NoError(t, err, "Error should not be present!") + assert.False(t, equal, "Bech addresses should not be equal!") +} + +func TestBoolToFloat64(t *testing.T) { + t.Parallel() + assert.InDelta(t, float64(1), BoolToFloat64(true), 0.001) + assert.InDelta(t, float64(0), BoolToFloat64(false), 0.001) +} + +func TestStrToFloat64(t *testing.T) { + t.Parallel() + assert.InDelta(t, 1.234, StrToFloat64("1.234"), 0.001) +} + +func TestStrToFloat64Invalid(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + require.Fail(t, "Expected to have a panic here!") + } + }() + + StrToFloat64("test") +} + +func TestStrToInt64(t *testing.T) { + t.Parallel() + assert.InDelta(t, int64(1234), StrToInt64("1234"), 0.001) +} + +func TestStrToInt64Invalid(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + require.Fail(t, "Expected to have a panic here!") + } + }() + + StrToInt64("test") +} + +func TestFilter(t *testing.T) { + t.Parallel() + + array := []string{"true", "false"} + filtered := Filter(array, func(s string) bool { + return s == "true" + }) + + assert.Len(t, filtered, 1, "Array should have 1 entry!") + assert.Equal(t, "true", filtered[0], "Value mismatch!") +} + +func TestMap(t *testing.T) { + t.Parallel() + + array := []int{2, 4} + filtered := Map(array, func(v int) int { + return v * 2 + }) + + assert.Len(t, filtered, 2, "Array should have 2 entries!") + assert.Equal(t, 4, filtered[0], "Value mismatch!") + assert.Equal(t, 8, filtered[1], "Value mismatch!") +} + +func TestFind(t *testing.T) { + t.Parallel() + + array := []TestCompareStruct{{value: 2}, {value: 4}} + value, found := Find(array, func(v TestCompareStruct) bool { + return v.value == 2 + }) + + assert.True(t, found) + assert.NotNil(t, value) + assert.Equal(t, 2, value.value) + + value2, found2 := Find(array, func(v TestCompareStruct) bool { + return v.value == 3 + }) + + assert.Nil(t, value2) + assert.False(t, found2) +} + +func TestFindIndex(t *testing.T) { + t.Parallel() + + array := []TestCompareStruct{{value: 2}, {value: 4}} + index, found := FindIndex(array, func(v TestCompareStruct) bool { + return v.value == 4 + }) + + assert.True(t, found) + assert.Equal(t, 1, index) + + value2, found2 := FindIndex(array, func(v TestCompareStruct) bool { + return v.value == 3 + }) + + assert.Equal(t, 0, value2) + assert.False(t, found2) +} + +func TestChangeBech32Prefix(t *testing.T) { + t.Parallel() + + _, err := ChangeBech32Prefix("test", "test") + require.Error(t, err) + + value, err := ChangeBech32Prefix("cosmos1xqz9pemz5e5zycaa89kys5aw6m8rhgsvtp9lt2", "cosmosvaloper") + require.NoError(t, err) + require.Equal(t, "cosmosvaloper1xqz9pemz5e5zycaa89kys5aw6m8rhgsvw4328e", value) +} + +func TestGetBlockFromHeaderNoValue(t *testing.T) { + t.Parallel() + + header := http.Header{} + value, err := GetBlockHeightFromHeader(header) + + require.NoError(t, err) + assert.Equal(t, int64(0), value) +} + +func TestGetBlockFromHeaderInvalidValue(t *testing.T) { + t.Parallel() + + header := http.Header{ + constants.HeaderBlockHeight: []string{"invalid"}, + } + value, err := GetBlockHeightFromHeader(header) + + require.Error(t, err) + assert.Equal(t, int64(0), value) +} + +func TestGetBlockFromHeaderValidValue(t *testing.T) { + t.Parallel() + + header := http.Header{ + constants.HeaderBlockHeight: []string{"123"}, + } + value, err := GetBlockHeightFromHeader(header) + + require.NoError(t, err) + assert.Equal(t, int64(123), value) +}