chore: update golangci-lint to v2.12.2 with canonical config
All checks were successful
check / check (push) Successful in 2m3s
All checks were successful
check / check (push) Successful in 2m3s
Replace .golangci.yml with the canonical v2-schema config (default: all minus six disabled linters, lll 88, tests included) and bump every golangci-lint pin to v2.12.2: - Dockerfile: golangci/golangci-lint:v2.12.2-alpine (hash-pinned) - script/bootstrap: GOLANGCI_LINT_VERSION 2.12.2 with new linux-amd64/arm64 release-archive sha256 pins Fix all 747 findings the stricter config surfaces, with no behavior changes: t.Parallel() throughout the test suite, static sentinel errors and errors.Is comparisons, checked error returns, context propagation (contextcheck/noctx), 88-column wrapping, extracted constants and helpers for goconst/dupl/funlen/cyclop, exhaustive switch cases replicating existing defaults, and white-box test files renamed to *_internal_test.go for testpackage. Three nolint:tagliatelle directives preserve the existing snake_case JSON wire and on-disk metadata formats.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
@@ -24,10 +25,17 @@ const (
|
||||
// Params defines dependencies for Config.
|
||||
type Params struct {
|
||||
fx.In
|
||||
|
||||
Globals *globals.Globals
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
// Static validation errors.
|
||||
var (
|
||||
errSigningKeyRequired = errors.New("signing_key is required")
|
||||
errSigningKeyTooShort = errors.New("signing_key too short")
|
||||
)
|
||||
|
||||
// Config holds application configuration values.
|
||||
type Config struct {
|
||||
Debug bool
|
||||
@@ -61,17 +69,19 @@ func New(_ fx.Lifecycle, params Params) (*Config, error) {
|
||||
}
|
||||
|
||||
c := &Config{
|
||||
Debug: getBool(sc, "debug", false),
|
||||
MaintenanceMode: getBool(sc, "maintenance_mode", false),
|
||||
Port: getInt(sc, "port", DefaultPort),
|
||||
StateDir: getString(sc, "state_dir", DefaultStateDir),
|
||||
SentryDSN: getString(sc, "sentry_dsn", ""),
|
||||
MetricsUsername: getString(sc, "metrics.username", ""),
|
||||
MetricsPassword: getString(sc, "metrics.password", ""),
|
||||
SigningKey: getString(sc, "signing_key", ""),
|
||||
AllowlistHosts: getStringSlice(sc, "allowlist_hosts"),
|
||||
AllowHTTP: getBool(sc, "allow_http", false),
|
||||
UpstreamConnectionsPerHost: getInt(sc, "upstream_connections_per_host", DefaultUpstreamConnectionsPerHost),
|
||||
Debug: getBool(sc, "debug", false),
|
||||
MaintenanceMode: getBool(sc, "maintenance_mode", false),
|
||||
Port: getInt(sc, "port", DefaultPort),
|
||||
StateDir: getString(sc, "state_dir", DefaultStateDir),
|
||||
SentryDSN: getString(sc, "sentry_dsn", ""),
|
||||
MetricsUsername: getString(sc, "metrics.username", ""),
|
||||
MetricsPassword: getString(sc, "metrics.password", ""),
|
||||
SigningKey: getString(sc, "signing_key", ""),
|
||||
AllowlistHosts: getStringSlice(sc),
|
||||
AllowHTTP: getBool(sc, "allow_http", false),
|
||||
UpstreamConnectionsPerHost: getInt(
|
||||
sc, "upstream_connections_per_host", DefaultUpstreamConnectionsPerHost,
|
||||
),
|
||||
}
|
||||
|
||||
// Build DBURL from StateDir if not explicitly set
|
||||
@@ -85,7 +95,8 @@ func New(_ fx.Lifecycle, params Params) (*Config, error) {
|
||||
}
|
||||
|
||||
// Validate required configuration
|
||||
if err := c.validate(); err != nil {
|
||||
err = c.validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -95,19 +106,22 @@ func New(_ fx.Lifecycle, params Params) (*Config, error) {
|
||||
// validate checks that all required configuration values are set.
|
||||
func (c *Config) validate() error {
|
||||
if c.SigningKey == "" {
|
||||
return fmt.Errorf("signing_key is required")
|
||||
return errSigningKeyRequired
|
||||
}
|
||||
|
||||
// Minimum key length for security (32 bytes = 256 bits)
|
||||
const minKeyLength = 32
|
||||
if len(c.SigningKey) < minKeyLength {
|
||||
return fmt.Errorf("signing_key must be at least %d characters", minKeyLength)
|
||||
return fmt.Errorf(
|
||||
"%w: must be at least %d characters", errSigningKeyTooShort, minKeyLength,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfigFile loads configuration from PIXA_CONFIG_PATH env var or standard locations.
|
||||
// loadConfigFile loads configuration from the PIXA_CONFIG_PATH env var
|
||||
// or standard locations.
|
||||
func loadConfigFile(log *slog.Logger, appName string) (*smartconfig.Config, error) {
|
||||
// Check for explicit config path from environment
|
||||
if envPath := os.Getenv("PIXA_CONFIG_PATH"); envPath != "" {
|
||||
@@ -133,8 +147,9 @@ func loadConfigFile(log *slog.Logger, appName string) (*smartconfig.Config, erro
|
||||
|
||||
for _, path := range configPaths {
|
||||
cleanPath := filepath.Clean(path)
|
||||
//nolint:gosec // G703: paths are hardcoded config locations
|
||||
if _, statErr := os.Stat(cleanPath); statErr == nil {
|
||||
|
||||
_, statErr := os.Stat(cleanPath)
|
||||
if statErr == nil {
|
||||
sc, err := smartconfig.NewFromConfigPath(path)
|
||||
if err != nil {
|
||||
log.Warn("failed to parse config file", "path", path, "error", err)
|
||||
@@ -190,18 +205,18 @@ func getBool(sc *smartconfig.Config, key string, defaultVal bool) bool {
|
||||
return val
|
||||
}
|
||||
|
||||
func getStringSlice(sc *smartconfig.Config, key string) []string {
|
||||
func getStringSlice(sc *smartconfig.Config) []string {
|
||||
if sc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
val, ok := sc.Get(key)
|
||||
val, ok := sc.Get("allowlist_hosts")
|
||||
if !ok || val == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle YAML list format
|
||||
if slice, ok := val.([]interface{}); ok {
|
||||
if slice, ok := val.([]any); ok {
|
||||
result := make([]string, 0, len(slice))
|
||||
for _, item := range slice {
|
||||
if str, ok := item.(string); ok {
|
||||
|
||||
98
internal/config/config_internal_test.go
Normal file
98
internal/config/config_internal_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/smartconfig"
|
||||
)
|
||||
|
||||
// writeTestConfig writes yamlContent to a temp config file and returns
|
||||
// the file path.
|
||||
func writeTestConfig(t *testing.T, yamlContent string) string {
|
||||
t.Helper()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
err := os.WriteFile(configPath, []byte(yamlContent), 0o600)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
return configPath
|
||||
}
|
||||
|
||||
// checkAllowlistHosts loads the config at configPath and asserts that
|
||||
// getStringSlice returns the three expected hosts.
|
||||
func checkAllowlistHosts(t *testing.T, configPath string) {
|
||||
t.Helper()
|
||||
|
||||
sc, err := loadTestConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
hosts := getStringSlice(sc)
|
||||
|
||||
if len(hosts) != 3 {
|
||||
t.Errorf("expected 3 hosts, got %d: %v", len(hosts), hosts)
|
||||
}
|
||||
|
||||
expected := []string{"static.sneak.cloud", "sneak.berlin", "s3.sneak.cloud"}
|
||||
for i, want := range expected {
|
||||
if i >= len(hosts) {
|
||||
t.Errorf("missing host at index %d: want %q", i, want)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if hosts[i] != want {
|
||||
t.Errorf("host[%d] = %q, want %q", i, hosts[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringSlice_YAMLList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
yamlContent := `
|
||||
allowlist_hosts:
|
||||
- static.sneak.cloud
|
||||
- sneak.berlin
|
||||
- s3.sneak.cloud
|
||||
`
|
||||
|
||||
checkAllowlistHosts(t, writeTestConfig(t, yamlContent))
|
||||
}
|
||||
|
||||
func TestGetStringSlice_CommaSeparated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Backwards compatibility with comma-separated string values.
|
||||
yamlContent := `allowlist_hosts: "static.sneak.cloud, sneak.berlin, s3.sneak.cloud"`
|
||||
|
||||
checkAllowlistHosts(t, writeTestConfig(t, yamlContent))
|
||||
}
|
||||
|
||||
func TestGetStringSlice_Empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
configPath := writeTestConfig(t, `port: 8080`)
|
||||
|
||||
sc, err := loadTestConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
hosts := getStringSlice(sc)
|
||||
if len(hosts) != 0 {
|
||||
t.Errorf("expected nil or empty slice, got %v", hosts)
|
||||
}
|
||||
}
|
||||
|
||||
// loadTestConfig is a helper to load a config file for testing.
|
||||
func loadTestConfig(path string) (*smartconfig.Config, error) {
|
||||
return smartconfig.NewFromConfigPath(path)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/smartconfig"
|
||||
)
|
||||
|
||||
func TestGetStringSlice_YAMLList(t *testing.T) {
|
||||
// Create a temp config file with YAML list format
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
yamlContent := `
|
||||
allowlist_hosts:
|
||||
- static.sneak.cloud
|
||||
- sneak.berlin
|
||||
- s3.sneak.cloud
|
||||
`
|
||||
err := os.WriteFile(configPath, []byte(yamlContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load config using smartconfig
|
||||
sc, err := loadTestConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Test that getStringSlice correctly parses YAML list
|
||||
hosts := getStringSlice(sc, "allowlist_hosts")
|
||||
|
||||
if len(hosts) != 3 {
|
||||
t.Errorf("expected 3 hosts, got %d: %v", len(hosts), hosts)
|
||||
}
|
||||
|
||||
expected := []string{"static.sneak.cloud", "sneak.berlin", "s3.sneak.cloud"}
|
||||
for i, want := range expected {
|
||||
if i >= len(hosts) {
|
||||
t.Errorf("missing host at index %d: want %q", i, want)
|
||||
continue
|
||||
}
|
||||
if hosts[i] != want {
|
||||
t.Errorf("host[%d] = %q, want %q", i, hosts[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringSlice_CommaSeparated(t *testing.T) {
|
||||
// Test backwards compatibility with comma-separated string
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
yamlContent := `allowlist_hosts: "static.sneak.cloud, sneak.berlin, s3.sneak.cloud"`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(yamlContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
sc, err := loadTestConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
hosts := getStringSlice(sc, "allowlist_hosts")
|
||||
|
||||
if len(hosts) != 3 {
|
||||
t.Errorf("expected 3 hosts, got %d: %v", len(hosts), hosts)
|
||||
}
|
||||
|
||||
expected := []string{"static.sneak.cloud", "sneak.berlin", "s3.sneak.cloud"}
|
||||
for i, want := range expected {
|
||||
if i >= len(hosts) {
|
||||
t.Errorf("missing host at index %d: want %q", i, want)
|
||||
continue
|
||||
}
|
||||
if hosts[i] != want {
|
||||
t.Errorf("host[%d] = %q, want %q", i, hosts[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringSlice_Empty(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
yamlContent := `port: 8080`
|
||||
|
||||
err := os.WriteFile(configPath, []byte(yamlContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
sc, err := loadTestConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
hosts := getStringSlice(sc, "allowlist_hosts")
|
||||
|
||||
if hosts != nil && len(hosts) != 0 {
|
||||
t.Errorf("expected nil or empty slice, got %v", hosts)
|
||||
}
|
||||
}
|
||||
|
||||
// loadTestConfig is a helper to load a config file for testing
|
||||
func loadTestConfig(path string) (*smartconfig.Config, error) {
|
||||
return smartconfig.NewFromConfigPath(path)
|
||||
}
|
||||
Reference in New Issue
Block a user