diff --git a/cmd/launchrail/launchrail.go b/cmd/launchrail/main.go similarity index 97% rename from cmd/launchrail/launchrail.go rename to cmd/launchrail/main.go index bfcb6eb..587a0d5 100644 --- a/cmd/launchrail/launchrail.go +++ b/cmd/launchrail/main.go @@ -1,4 +1,4 @@ -package launchrail +package main import ( "github.com/bxrne/launchrail/internal/config" @@ -9,7 +9,7 @@ import ( "github.com/bxrne/launchrail/pkg/thrustcurves" ) -func Root() { +func main() { log := logger.GetLogger() log.Debug("Starting...") diff --git a/config.yaml b/config.yaml index 84f59b1..e340c85 100644 --- a/config.yaml +++ b/config.yaml @@ -13,10 +13,9 @@ options: openrocket_file: "./testdata/openrocket/l1.ork" launchrail: length: 3.0 - angle: 0.0 - orientation: 0.0 + angle: 2.0 + orientation: 1.0 launchsite: latitude: 37.7749 longitude: -122.4194 - altitude: 0.0 - + altitude: 1.0 diff --git a/internal/config/config.go b/internal/config/config.go index 02e80af..97c6f0c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,36 +1,40 @@ package config import ( - "errors" "fmt" "os" "github.com/spf13/viper" ) -// GetConfig returns the singleton instance of the configuration. +var ( + cfg *Config +) + +// GetConfig returns the application configuration as a singleton func GetConfig() (*Config, error) { - var cfg *Config + v := viper.New() v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") if err := v.ReadInConfig(); err != nil { - return nil, errors.New("failed to read config file") + return nil, fmt.Errorf("failed to read config file: %s", err) } if err := v.Unmarshal(&cfg); err != nil { - return nil, errors.New("failed to unmarshal config") + return nil, fmt.Errorf("failed to unmarshal config: %s", err) } if err := cfg.Validate(); err != nil { - return nil, err + return nil, fmt.Errorf("failed to validate config: %s", err) } return cfg, nil } +// Validate checks the config to error on empty field func (cfg *Config) Validate() error { if cfg.App.Name == "" { return fmt.Errorf("app.name is required") @@ -44,6 +48,10 @@ func (cfg *Config) Validate() error { return fmt.Errorf("logging.level is required") } + if cfg.External.OpenRocketVersion == "" { + return fmt.Errorf("external.openrocket_version is required") + } + if cfg.Options.MotorDesignation == "" { return fmt.Errorf("options.motor_designation is required") } @@ -56,5 +64,29 @@ func (cfg *Config) Validate() error { return fmt.Errorf("options.openrocket_file is invalid: %s", err) } + if cfg.Options.Launchrail.Length == 0 { + return fmt.Errorf("options.launchrail.length is required") + } + + if cfg.Options.Launchrail.Angle == 0 { + return fmt.Errorf("options.launchrail.angle is required") + } + + if cfg.Options.Launchrail.Orientation == 0 { + return fmt.Errorf("options.launchrail.orientation is required") + } + + if cfg.Options.Launchsite.Latitude == 0 { + return fmt.Errorf("options.launchsite.latitude is required") + } + + if cfg.Options.Launchsite.Longitude == 0 { + return fmt.Errorf("options.launchsite.longitude is required") + } + + if cfg.Options.Launchsite.Altitude == 0 { + return fmt.Errorf("options.launchsite.altitude is required") + } + return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 922066c..8641114 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -8,7 +8,7 @@ import ( ) // Helper to change directory and reset after test -func withWorkingDir(t *testing.T, dir string, testFunc func()) { +func withWorkingDir(t *testing.T, dir string, testFunc func(cfg *config.Config, err error)) { originalDir, err := os.Getwd() if err != nil { t.Fatalf("Failed to get current directory: %s", err) @@ -26,14 +26,14 @@ func withWorkingDir(t *testing.T, dir string, testFunc func()) { } }() - // Run the test function - testFunc() + // WARN: Run the test function with the configuration and handle its error within + cfg, err := config.GetConfig() + testFunc(cfg, err) } // TEST: GIVEN a valid configuration file WHEN GetConfig is called THEN no error is returned func TestGetConfig(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } @@ -46,35 +46,54 @@ func TestGetConfig(t *testing.T) { // TEST: GIVEN an invalid config file WHEN GetConfig is called THEN the error 'failed to read config file' is returned func TestGetConfigInvalidConfigFile(t *testing.T) { - withWorkingDir(t, ".", func() { - _, err := config.GetConfig() + withWorkingDir(t, ".", func(cfg *config.Config, err error) { if err == nil { t.Error("Expected an error, got nil") } - if err.Error() != "failed to read config file" { - t.Errorf("Expected error to be 'failed to read config file', got: %s", err) + expected := "failed to read config file:" + if err.Error()[:len(expected)] != expected { + t.Errorf("Expected %s, got %s", expected, err) } }) } // TEST: GIVEN a bad config file WHEN GetConfig is called THEN the error 'failed to unmarshal config' is returned func TestGetConfigBadConfigFile(t *testing.T) { - withWorkingDir(t, "../../testdata/config/bad/", func() { - _, err := config.GetConfig() + withWorkingDir(t, "../../testdata/config/bad", func(cfg *config.Config, err error) { if err == nil { t.Error("Expected an error, got nil") } - if err.Error() != "failed to unmarshal config" { - t.Errorf("Expected error to be 'failed to unmarshal config', got: %s", err) + + expected := "failed to unmarshal config" + if err.Error()[:len(expected)] != expected { + t.Errorf("Expected %s, got %s", expected, err) } + }) } -// TEST: GIVEN a configuration file with missing app.name WHEN Validate is called THEN an error is returned -func TestValidateConfigMissingAppName(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() +// TEST: GIVEN a config WHEN another config is requested THEN the config is a singleton +func TestGetConfigSingleton(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { + if err != nil { + t.Errorf("Expected no error, got: %s", err) + } + + cfg2, err := config.GetConfig() + if err != nil { + t.Errorf("Expected no error, got: %s", err) + } + + if cfg != cfg2 { + t.Error("Expected config to be a singleton") + } + }) +} + +// TEST: GIVEN a config with missing app.name WHEN Validate is called THEN an error is returned +func TestGetConfigMissingFields(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } @@ -85,18 +104,16 @@ func TestValidateConfigMissingAppName(t *testing.T) { t.Error("Expected an error, got nil") } - if err.Error() != "app.name is required" { - t.Errorf("Expected error to be 'app.name is required', got: %s", err) + expected := "app.name is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) } - - cfg.App.Name = "launchrail-test" // Reset app.name }) } -// TEST: GIVEN a configuration file with missing app.version WHEN GetConfig is called THEN an error is returned -func TestValidateConfigMissingAppVersion(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() +// TEST: GIVEN a config with missing app.version WHEN Validate is called THEN an error is returned +func TestGetConfigMissingVersion(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } @@ -107,18 +124,16 @@ func TestValidateConfigMissingAppVersion(t *testing.T) { t.Error("Expected an error, got nil") } - if err.Error() != "app.version is required" { - t.Errorf("Expected error to be 'app.version is required', got: %s", err) + expected := "app.version is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) } - - cfg.App.Version = "0.0.0" // Reset app.version }) } -// TEST: GIVEN a configuration file with missing logging.level WHEN GetConfig is called THEN an error is returned -func TestValidateConfigMissingLoggingLevel(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() +// TEST: GIVEN a config with missing logging.level WHEN Validate is called THEN an error is returned +func TestGetConfigMissingLoggingLevel(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } @@ -129,97 +144,189 @@ func TestValidateConfigMissingLoggingLevel(t *testing.T) { t.Error("Expected an error, got nil") } - if err.Error() != "logging.level is required" { - t.Errorf("Expected error to be 'logging.level is required', got: %s", err) + expected := "logging.level is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) } - - cfg.Logging.Level = "info" // Reset logging.level }) } -// TEST: GIVEN a configuration file with missing external.openrocket_version WHEN GetConfig is called THEN no error is returned -func TestValidateConfigMissingOpenRocketVersion(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() +// TEST: GIVEN a config with external.openrocket_version WHEN Validate is called THEN no error is returned +func TestGetConfigExternalOpenRocketVersion(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } cfg.External.OpenRocketVersion = "" err = cfg.Validate() + if err == nil { + t.Error("Expected an error, got nil") + } + + expected := "external.openrocket_version is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } + }) +} + +// TEST: GIVEN a config with missing options.motor_designation WHEN Validate is called THEN an error is returned +func TestGetConfigMissingMotorDesignation(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } - cfg.External.OpenRocketVersion = "15.03" // Reset external.openrocket_version + cfg.Options.MotorDesignation = "" + err = cfg.Validate() + if err == nil { + t.Error("Expected an error, got nil") + } + + expected := "options.motor_designation is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } }) } -// TEST: GIVEN a configuration file with missing options.motor_designation WHEN GetConfig is called THEN an error is returned -func TestValidateConfigMissingMotorDesignation(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() +// TEST: GIVEN a config with missing options.openrocket_file WHEN Validate is called THEN an error is returned +func TestGetConfigMissingOpenRocketFile(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } - cfg.Options.MotorDesignation = "" + cfg.Options.OpenRocketFile = "" err = cfg.Validate() if err == nil { t.Error("Expected an error, got nil") } - if err.Error() != "options.motor_designation is required" { - t.Errorf("Expected error to be 'options.motor_designation is required', got: %s", err) + expected := "options.openrocket_file is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } + }) +} + +// TEST: GIVEN a config with missing options.launchrail.length WHEN Validate is called THEN no error is returned +func TestGetConfigMissingLaunchrailLength(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { + if err != nil { + t.Errorf("Expected no error, got: %s", err) } - cfg.Options.MotorDesignation = "A8" // Reset options.motor_designation + cfg.Options.Launchrail.Length = 0 + err = cfg.Validate() + if err == nil { + t.Error("Expected an error, got nil") + } + + expected := "options.launchrail.length is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } }) } -// TEST: GIVEN a configuration file with missing options.open_rocket_file WHEN GetConfig is called THEN an error is returned -func TestValidateConfigMissingOpenRocketFile(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() +// TEST: GIVEN a config with missing options.launchrail.angle WHEN Validate is called THEN no error is returned +func TestGetConfigMissingLaunchrailAngle(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } - cfg.Options.OpenRocketFile = "" + cfg.Options.Launchrail.Angle = 0 err = cfg.Validate() if err == nil { t.Error("Expected an error, got nil") } - if err.Error() != "options.openrocket_file is required" { - t.Errorf("Expected error to be 'options.openrocket_file is required', got: %s", err) + expected := "options.launchrail.angle is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } + }) +} + +// TEST: GIVEN a config with missing options.launchrail.orientation WHEN Validate is called THEN no error is returned +func TestGetConfigMissingLaunchrailOrientation(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { + if err != nil { + t.Errorf("Expected no error, got: %s", err) } - cfg.Options.OpenRocketFile = "./testdata/openrocket/l1.ork" // Reset options.open_rocket_file + cfg.Options.Launchrail.Orientation = 0 + err = cfg.Validate() + if err == nil { + t.Error("Expected an error, got nil") + } + + expected := "options.launchrail.orientation is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } }) } -// TEST: GIVEN a configuration file with invalid options.open_rocket_file WHEN GetConfig is called THEN an error is returned -func TestValidateConfigInvalidOpenRocketFile(t *testing.T) { - withWorkingDir(t, "../..", func() { - cfg, err := config.GetConfig() +// TEST: GIVEN a config with missing options.launchsite.latitude WHEN Validate is called THEN no error is returned +func TestGetConfigMissingLaunchsiteLatitude(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { if err != nil { t.Errorf("Expected no error, got: %s", err) } - cfg.Options.OpenRocketFile = "test/resources/invalid.ork" + cfg.Options.Launchsite.Latitude = 0 err = cfg.Validate() if err == nil { t.Error("Expected an error, got nil") } - unixErr := "options.openrocket_file is invalid: stat test/resources/invalid.ork: no such file or directory" - winErr := "options.openrocket_file is invalid: CreateFile test/resources/invalid.ork: The system cannot find the path specified." + expected := "options.launchsite.latitude is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } + }) +} - if err.Error() != unixErr && err.Error() != winErr { - t.Errorf("Expected error to be '%s' or '%s', got: %s", unixErr, winErr, err) +// TEST: GIVEN a config with missing options.launchsite.longitude WHEN Validate is called THEN no error is returned +func TestGetConfigMissingLaunchsiteLongitude(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { + if err != nil { + t.Errorf("Expected no error, got: %s", err) + } + + cfg.Options.Launchsite.Longitude = 0 + err = cfg.Validate() + if err == nil { + t.Error("Expected an error, got nil") + } + + expected := "options.launchsite.longitude is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } + }) +} + +// TEST: GIVEN a config with missing options.launchsite.altitude WHEN Validate is called THEN no error is returned +func TestGetConfigMissingLaunchsiteAltitude(t *testing.T) { + withWorkingDir(t, "../..", func(cfg *config.Config, err error) { + if err != nil { + t.Errorf("Expected no error, got: %s", err) + } + + cfg.Options.Launchsite.Altitude = 0 + err = cfg.Validate() + if err == nil { + t.Error("Expected an error, got nil") } - cfg.Options.OpenRocketFile = "test/resources/rocket.ork" // Reset options.open_rocket_file + expected := "options.launchsite.altitude is required" + if err.Error() != expected { + t.Errorf("Expected %s, got %s", expected, err) + } }) } diff --git a/internal/config/schema.go b/internal/config/schema.go index 7352284..82539a2 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -30,7 +30,7 @@ type Config struct { } `mapstructure:"options"` } -// Marshal to map structure for logging. +// String returns the configuration as a map of strings, useful for testing. func (c *Config) String() map[string]string { marshalled := make(map[string]string) marshalled["app.name"] = c.App.Name diff --git a/internal/http_client/mock_client_test.go b/internal/http_client/mock_client_test.go index 87ad9aa..3de5777 100644 --- a/internal/http_client/mock_client_test.go +++ b/internal/http_client/mock_client_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) +// TEST: GIVEN a valid configuration file WHEN GetConfig is called THEN no error is returned func TestMockHTTPClient_Post(t *testing.T) { tests := []struct { name string diff --git a/main.go b/main.go deleted file mode 100644 index e8c4cd8..0000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/bxrne/launchrail/cmd/launchrail" -) - -func main() { - launchrail.Root() -} diff --git a/sonar-project.properties b/sonar-project.properties index db35a8b..b383990 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -7,5 +7,5 @@ sonar.sources=. sonar.exclusions=**/*_test.go,**/vendor/**,**/cmd/**,*.xml sonar.tests=. sonar.test.inclusions=**/*_test.go -sonar.test.exclusions=**/vendor/** +sonar.test.exclusions=**/vendor/**, **/cmd/** sonar.go.coverage.reportPaths=coverage.out