From bf1264a77c951823dc03605b19de7144a977a5f3 Mon Sep 17 00:00:00 2001 From: adcondev <38170282+adcondev@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:26:15 +0000 Subject: [PATCH 1/2] test: add GetEnvironment tests Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- internal/config/config_test.go | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 internal/config/config_test.go diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..bda674b --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,100 @@ +package config + +import ( + "bytes" + "log" + "strings" + "testing" +) + +func TestGetEnvironment(t *testing.T) { + tests := []struct { + name string + env string + wantName string + wantLog bool + }{ + { + name: "Valid remote environment", + env: "remote", + wantName: "REMOTO", + wantLog: false, + }, + { + name: "Valid local environment", + env: "local", + wantName: "LOCAL", + wantLog: false, + }, + { + name: "Unknown environment falls back to local", + env: "unknown", + wantName: "LOCAL", + wantLog: true, + }, + { + name: "Empty environment falls back to local", + env: "", + wantName: "LOCAL", + wantLog: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&buf) + t.Cleanup(func() { + log.SetOutput(originalOutput) + }) + + got := GetEnvironment(tt.env) + + if got.Name != tt.wantName { + t.Errorf("GetEnvironment(%q) got environment name = %v, want %v", tt.env, got.Name, tt.wantName) + } + + if tt.wantLog { + if buf.Len() == 0 { + t.Errorf("GetEnvironment(%q) expected log output, but got none", tt.env) + } + } else { + if buf.Len() > 0 { + t.Errorf("GetEnvironment(%q) unexpected log output: %v", tt.env, buf.String()) + } + } + }) + } +} + +func TestEnvironmentsConsistency(t *testing.T) { + // The Environments map is initialized with global variables that can be overridden via ldflags. + // This test ensures that the map entries accurately reflect the current state of these globals. + + // ServerPort is empty by default + expectedListenSuffix := ServerPort + + // We test what is currently set in the global state + remoteEnv := Environments["remote"] + if remoteEnv.Name != "REMOTO" { + t.Errorf("Expected 'remote' environment Name to be 'REMOTO', got %s", remoteEnv.Name) + } + if !strings.HasSuffix(remoteEnv.ListenAddr, expectedListenSuffix) { + t.Errorf("Expected 'remote' ListenAddr to end with %q, got %s", expectedListenSuffix, remoteEnv.ListenAddr) + } + if remoteEnv.DefaultPort != "COM3" { + t.Errorf("Expected 'remote' DefaultPort to be 'COM3', got %s", remoteEnv.DefaultPort) + } + + localEnv := Environments["local"] + if localEnv.Name != "LOCAL" { + t.Errorf("Expected 'local' environment Name to be 'LOCAL', got %s", localEnv.Name) + } + if !strings.HasSuffix(localEnv.ListenAddr, expectedListenSuffix) { + t.Errorf("Expected 'local' ListenAddr to end with %q, got %s", expectedListenSuffix, localEnv.ListenAddr) + } + if localEnv.DefaultPort != "COM3" { + t.Errorf("Expected 'local' DefaultPort to be 'COM3', got %s", localEnv.DefaultPort) + } +} From 1d41927d5bc72c463925c86ffde10bba6e4d0671 Mon Sep 17 00:00:00 2001 From: adcondev <38170282+adcondev@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:50:59 +0000 Subject: [PATCH 2/2] test: add GetEnvironment tests and fix lint issues Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- internal/auth/auth.go | 2 ++ internal/config/config.go | 15 +++++++++++---- internal/config/config_test.go | 16 ++++++++-------- internal/logging/logging.go | 16 +++++++++------- internal/logging/rotation.go | 15 +++++++++------ internal/server/models.go | 2 +- internal/server/server.go | 8 ++++---- 7 files changed, 44 insertions(+), 30 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 466c467..0ecdd68 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -161,6 +161,7 @@ func (m *Manager) SetSessionCookie(w http.ResponseWriter) string { Path: "/", MaxAge: int(SessionDuration.Seconds()), HttpOnly: true, + Secure: true, SameSite: http.SameSiteStrictMode, }) return token @@ -174,6 +175,7 @@ func (m *Manager) ClearSessionCookie(w http.ResponseWriter) { Path: "/", MaxAge: -1, HttpOnly: true, + Secure: true, SameSite: http.SameSiteStrictMode, }) } diff --git a/internal/config/config.go b/internal/config/config.go index 6d3918a..994304b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,23 +35,30 @@ type Environment struct { DefaultPort string DefaultMode bool // true = test mode (simulated weights), false = real weights } +// Common configuration constants +const ( + EnvRemoteName = "REMOTO" + EnvLocalName = "LOCAL" + DefaultComPort = "COM3" +) + // TODO: Make Port inyectable via ldflags. Same port and addres could cause conflicts // Environments defines available deployment configurations var Environments = map[string]Environment{ "remote": { - Name: "REMOTO", + Name: EnvRemoteName, ServiceName: ServiceName, ListenAddr: "0.0.0.0:" + ServerPort, - DefaultPort: "COM3", + DefaultPort: DefaultComPort, DefaultMode: false, }, "local": { - Name: "LOCAL", + Name: EnvLocalName, ServiceName: ServiceName, ListenAddr: "localhost:" + ServerPort, - DefaultPort: "COM3", + DefaultPort: DefaultComPort, DefaultMode: false, }, } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bda674b..fddd8aa 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -17,25 +17,25 @@ func TestGetEnvironment(t *testing.T) { { name: "Valid remote environment", env: "remote", - wantName: "REMOTO", + wantName: EnvRemoteName, wantLog: false, }, { name: "Valid local environment", env: "local", - wantName: "LOCAL", + wantName: EnvLocalName, wantLog: false, }, { name: "Unknown environment falls back to local", env: "unknown", - wantName: "LOCAL", + wantName: EnvLocalName, wantLog: true, }, { name: "Empty environment falls back to local", env: "", - wantName: "LOCAL", + wantName: EnvLocalName, wantLog: true, }, } @@ -77,24 +77,24 @@ func TestEnvironmentsConsistency(t *testing.T) { // We test what is currently set in the global state remoteEnv := Environments["remote"] - if remoteEnv.Name != "REMOTO" { + if remoteEnv.Name != EnvRemoteName { t.Errorf("Expected 'remote' environment Name to be 'REMOTO', got %s", remoteEnv.Name) } if !strings.HasSuffix(remoteEnv.ListenAddr, expectedListenSuffix) { t.Errorf("Expected 'remote' ListenAddr to end with %q, got %s", expectedListenSuffix, remoteEnv.ListenAddr) } - if remoteEnv.DefaultPort != "COM3" { + if remoteEnv.DefaultPort != DefaultComPort { t.Errorf("Expected 'remote' DefaultPort to be 'COM3', got %s", remoteEnv.DefaultPort) } localEnv := Environments["local"] - if localEnv.Name != "LOCAL" { + if localEnv.Name != EnvLocalName { t.Errorf("Expected 'local' environment Name to be 'LOCAL', got %s", localEnv.Name) } if !strings.HasSuffix(localEnv.ListenAddr, expectedListenSuffix) { t.Errorf("Expected 'local' ListenAddr to end with %q, got %s", expectedListenSuffix, localEnv.ListenAddr) } - if localEnv.DefaultPort != "COM3" { + if localEnv.DefaultPort != DefaultComPort { t.Errorf("Expected 'local' DefaultPort to be 'COM3', got %s", localEnv.DefaultPort) } } diff --git a/internal/logging/logging.go b/internal/logging/logging.go index fa56afa..eb5976f 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -77,12 +77,12 @@ func Setup(serviceName string, defaultVerbose bool) (*Manager, error) { mgr.FilePath = filepath.Join(logDir, serviceName+".log") // Try to create log directory - //nolint:gosec + if err := os.MkdirAll(logDir, 0750); err != nil { // Permission denied - fallback to stdout (console mode) log.SetOutput(os.Stdout) mgr.FilePath = "" - //nolint:gosec + log.Printf("[i] Logging to stdout (no write access to %q)", logDir) return mgr, nil } @@ -93,17 +93,18 @@ func Setup(serviceName string, defaultVerbose bool) (*Manager, error) { } // Open log file - f, err := os.OpenFile(mgr.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) + securePath := filepath.Clean(mgr.FilePath) + f, err := os.OpenFile(securePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) if err != nil { // Fallback to stdout log.SetOutput(os.Stdout) - log.Printf("[i] Logging to stdout (cannot open %s: %v)", mgr.FilePath, err) + log.Printf("[i] Logging to stdout (cannot open %q: %v)", filepath.Clean(mgr.FilePath), err) return mgr, nil } mgr.file = f log.SetOutput(NewFilteredLogger(f, &mgr.Verbose, &mgr.mu)) - log.Printf("[i] Logging to: %s", mgr.FilePath) + log.Printf("[i] Logging to: %q", filepath.Clean(mgr.FilePath)) return mgr, nil } @@ -151,10 +152,11 @@ func (m *Manager) Flush() error { } // Reopen file - f, err := os.OpenFile(m.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) + securePath := filepath.Clean(m.FilePath) + f, err := os.OpenFile(securePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) if err != nil { log.SetOutput(os.Stdout) - log.Printf("[i] Logging to stdout (cannot open %s: %v)", m.FilePath, err) + log.Printf("[i] Logging to stdout (cannot open %q: %v)", filepath.Clean(m.FilePath), err) return err } diff --git a/internal/logging/rotation.go b/internal/logging/rotation.go index 12eec34..3e16130 100644 --- a/internal/logging/rotation.go +++ b/internal/logging/rotation.go @@ -14,7 +14,8 @@ const MaxLogSize = 5 * 1024 * 1024 // RotateIfNeeded truncates the log file if it exceeds MaxLogSize // Keeps the last 1000 lines for continuity func RotateIfNeeded(path string) error { - info, err := os.Stat(path) + securePath := filepath.Clean(path) + info, err := os.Stat(securePath) if err != nil { if os.IsNotExist(err) { return nil @@ -32,7 +33,7 @@ func RotateIfNeeded(path string) error { } content := strings.Join(lines, "\n") + "\n" - return os.WriteFile(path, []byte(content), 0600) + return os.WriteFile(securePath, []byte(content), 0600) } // ReadLastNLines reads the last n lines from a file efficiently @@ -43,7 +44,7 @@ func ReadLastNLines(path string, n int) []string { if err != nil { return []string{} } - file, err := os.Open(securePath) //nolint:gosec + file, err := os.Open(securePath) if err != nil { return []string{} } @@ -101,17 +102,19 @@ func ReadLastNLines(path string, n int) []string { // Flush reduces the log file to the last 50 lines func Flush(path string) error { - lines := ReadLastNLines(path, 50) + securePath := filepath.Clean(path) + lines := ReadLastNLines(securePath, 50) content := "" if len(lines) > 0 { content = strings.Join(lines, "\n") + "\n" } - return os.WriteFile(path, []byte(content), 0600) + return os.WriteFile(securePath, []byte(content), 0600) } // GetFileSize returns the size of the log file in bytes func GetFileSize(path string) int64 { - info, err := os.Stat(path) + securePath := filepath.Clean(path) + info, err := os.Stat(securePath) if err != nil { return 0 } diff --git a/internal/server/models.go b/internal/server/models.go index 5e561d8..055bb04 100644 --- a/internal/server/models.go +++ b/internal/server/models.go @@ -9,7 +9,7 @@ type ConfigMessage struct { Marca string `json:"marca"` ModoPrueba bool `json:"modoPrueba"` Dir string `json:"dir,omitempty"` - //nolint:gosec + AuthToken string `json:"auth_token"` // Required for config changes } diff --git a/internal/server/server.go b/internal/server/server.go index ab92e9d..453f91f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -170,7 +170,7 @@ func (s *Server) serveDashboard(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") data := struct { - //nolint:gosec + AuthToken string }{ AuthToken: config.AuthToken, @@ -192,7 +192,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { // Check lockout FIRST if s.auth.IsLockedOut(ip) { - //nolint:gosec + log.Printf("[AUDIT] LOGIN_BLOCKED | IP=%q | reason=lockout", ip) http.Redirect(w, r, "/login?locked=1", http.StatusSeeOther) return @@ -201,7 +201,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") if !s.auth.ValidatePassword(password) { s.auth.RecordFailedLogin(ip) - //nolint:gosec + log.Printf("[AUDIT] LOGIN_FAILED | IP=%q", ip) http.Redirect(w, r, "/login?error=1", http.StatusSeeOther) return @@ -210,7 +210,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { // Success s.auth.ClearFailedLogins(ip) s.auth.SetSessionCookie(w) - //nolint:gosec + log.Printf("[AUDIT] LOGIN_SUCCESS | IP=%q", ip) http.Redirect(w, r, "/", http.StatusSeeOther) }