hako/internal/config/config_test.go
Felipe M. a7f3e73493
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
fix: allow startup with environment variables when config file is missing
The application now gracefully handles missing config files and can be
configured entirely through environment variables. File-not-found errors
are ignored while other errors (like permission denied) are still reported.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 15:46:00 +01:00

332 lines
8.9 KiB
Go

package config
import (
"net"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestConfig_LoadFromFile(t *testing.T) {
// Create a temporary config file
tmpFile, err := os.CreateTemp("", "config-test-*.yaml")
require.NoError(t, err)
defer func() {
_ = os.Remove(tmpFile.Name())
}()
configContent := `
database_url: "sqlite:test.db"
jwt_secret: "test-secret"
jwt_expiration: "2h"
server_port: 9090
archive_storage_path: "/tmp/archives"
`
_, err = tmpFile.WriteString(configContent)
require.NoError(t, err)
err = tmpFile.Close()
require.NoError(t, err)
// Load config
cfg, err := Load(tmpFile.Name())
require.NoError(t, err)
require.NotNil(t, cfg)
require.Equal(t, "sqlite:test.db", cfg.DatabaseURL)
require.Equal(t, "test-secret", cfg.JWTSecret)
require.Equal(t, 2*time.Hour, cfg.JWTExpiration)
require.Equal(t, 9090, cfg.ServerPort)
require.Equal(t, "/tmp/archives", cfg.ArchiveStoragePath)
}
func TestConfig_LoadFromEnv(t *testing.T) {
// Set environment variables
_ = os.Setenv("DATABASE_URL", "sqlite:env.db")
_ = os.Setenv("JWT_SECRET", "env-secret")
_ = os.Setenv("JWT_EXPIRATION", "3h")
_ = os.Setenv("SERVER_PORT", "8888")
_ = os.Setenv("ARCHIVE_STORAGE_PATH", "/env/archives")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
_ = os.Unsetenv("JWT_EXPIRATION")
_ = os.Unsetenv("SERVER_PORT")
_ = os.Unsetenv("ARCHIVE_STORAGE_PATH")
}()
// Load config without file (from env only)
cfg, err := Load("")
require.NoError(t, err)
require.NotNil(t, cfg)
require.Equal(t, "sqlite:env.db", cfg.DatabaseURL)
require.Equal(t, "env-secret", cfg.JWTSecret)
require.Equal(t, 3*time.Hour, cfg.JWTExpiration)
require.Equal(t, 8888, cfg.ServerPort)
require.Equal(t, "/env/archives", cfg.ArchiveStoragePath)
}
func TestConfig_Defaults(t *testing.T) {
// Set only required fields
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
_ = os.Setenv("JWT_SECRET", "secret")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
}()
cfg, err := Load("")
require.NoError(t, err)
// Check defaults
require.Equal(t, 8080, cfg.ServerPort)
require.Equal(t, 24*time.Hour, cfg.JWTExpiration)
require.Equal(t, "devdata/archives", cfg.ArchiveStoragePath)
}
func TestConfig_ReadURLFallback(t *testing.T) {
// Set only main database URL
_ = os.Setenv("DATABASE_URL", "sqlite:main.db")
_ = os.Setenv("JWT_SECRET", "secret")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
}()
cfg, err := Load("")
require.NoError(t, err)
// DatabaseReadURL should fallback to DatabaseURL when not specified
require.Equal(t, "sqlite:main.db", cfg.DatabaseURL)
require.Equal(t, "sqlite:main.db", cfg.DatabaseReadURL)
}
func TestConfig_SeparateReadURL(t *testing.T) {
// DATABASE_URL is used for writes, DATABASE_READ_URL for reads
_ = os.Setenv("DATABASE_URL", "sqlite:write.db")
_ = os.Setenv("DATABASE_READ_URL", "sqlite:read.db")
_ = os.Setenv("JWT_SECRET", "secret")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("DATABASE_READ_URL")
_ = os.Unsetenv("JWT_SECRET")
}()
cfg, err := Load("")
require.NoError(t, err)
require.Equal(t, "sqlite:write.db", cfg.DatabaseURL)
require.Equal(t, "sqlite:read.db", cfg.DatabaseReadURL)
}
func TestConfig_MissingRequired(t *testing.T) {
// Missing JWT secret
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
}()
_, err := Load("")
require.Error(t, err)
require.Contains(t, err.Error(), "jwt_secret")
}
func TestConfig_MissingDatabase(t *testing.T) {
// Missing database URL - use config file with empty database_url to override default
tmpFile, err := os.CreateTemp("", "config-test-*.yaml")
require.NoError(t, err)
defer func() {
_ = os.Remove(tmpFile.Name())
}()
configContent := `
database_url: ""
jwt_secret: "secret"
`
_, err = tmpFile.WriteString(configContent)
require.NoError(t, err)
err = tmpFile.Close()
require.NoError(t, err)
_, err = Load(tmpFile.Name())
require.Error(t, err)
require.Contains(t, err.Error(), "database")
}
func TestConfig_InvalidDuration(t *testing.T) {
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
_ = os.Setenv("JWT_SECRET", "secret")
_ = os.Setenv("JWT_EXPIRATION", "invalid-duration")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
_ = os.Unsetenv("JWT_EXPIRATION")
}()
_, err := Load("")
require.Error(t, err)
require.Contains(t, err.Error(), "jwt_expiration")
}
func TestConfig_FileNotFound(t *testing.T) {
// Test 1: Missing config file without env vars should fail validation (missing jwt_secret)
_, err := Load("/nonexistent/config.yaml")
require.Error(t, err)
require.Contains(t, err.Error(), "jwt_secret is required")
// Test 2: Missing config file with env vars should succeed
t.Setenv("JWT_SECRET", "test-secret")
cfg, err := Load("/nonexistent/config.yaml")
require.NoError(t, err)
require.Equal(t, "test-secret", cfg.JWTSecret)
require.Equal(t, "sqlite://devdata/hako.db", cfg.DatabaseURL) // Should use default
}
func TestConfig_ServerHost(t *testing.T) {
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
_ = os.Setenv("JWT_SECRET", "secret")
_ = os.Setenv("SERVER_HOST", "127.0.0.1")
_ = os.Setenv("SERVER_PORT", "9000")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
_ = os.Unsetenv("SERVER_HOST")
_ = os.Unsetenv("SERVER_PORT")
}()
cfg, err := Load("")
require.NoError(t, err)
require.Equal(t, "127.0.0.1", cfg.ServerHost)
require.Equal(t, 9000, cfg.ServerPort)
addr, err := cfg.GetBindAddress()
require.NoError(t, err)
require.Equal(t, "127.0.0.1:9000", addr)
url, err := cfg.GetServerURL()
require.NoError(t, err)
require.Equal(t, "http://127.0.0.1:9000", url)
}
func TestConfig_ServerHostAndInterfaceMutuallyExclusive(t *testing.T) {
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
_ = os.Setenv("JWT_SECRET", "secret")
_ = os.Setenv("SERVER_HOST", "127.0.0.1")
_ = os.Setenv("SERVER_INTERFACE", "eth0")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
_ = os.Unsetenv("SERVER_HOST")
_ = os.Unsetenv("SERVER_INTERFACE")
}()
_, err := Load("")
require.Error(t, err)
require.Contains(t, err.Error(), "server_host and server_interface cannot be defined at the same time")
}
func TestConfig_ServerInterface(t *testing.T) {
// Get a real interface name from the system
interfaces, err := net.Interfaces()
require.NoError(t, err)
if len(interfaces) == 0 {
t.Skip("No network interfaces available for testing")
}
// Find an interface with an IPv4 address
var testInterface string
for _, iface := range interfaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip != nil && ip.To4() != nil {
testInterface = iface.Name
break
}
}
if testInterface != "" {
break
}
}
if testInterface == "" {
t.Skip("No interface with IPv4 address found for testing")
}
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
_ = os.Setenv("JWT_SECRET", "secret")
_ = os.Setenv("SERVER_INTERFACE", testInterface)
_ = os.Setenv("SERVER_PORT", "9000")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
_ = os.Unsetenv("SERVER_INTERFACE")
_ = os.Unsetenv("SERVER_PORT")
}()
cfg, err := Load("")
require.NoError(t, err)
require.Equal(t, testInterface, cfg.ServerInterface)
require.Equal(t, 9000, cfg.ServerPort)
addr, err := cfg.GetBindAddress()
require.NoError(t, err)
require.Contains(t, addr, ":9000")
require.NotEqual(t, ":9000", addr) // Should have an IP address
url, err := cfg.GetServerURL()
require.NoError(t, err)
require.Contains(t, url, ":9000")
require.Contains(t, url, "http://")
}
func TestConfig_ServerInterfaceNotFound(t *testing.T) {
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
_ = os.Setenv("JWT_SECRET", "secret")
_ = os.Setenv("SERVER_INTERFACE", "nonexistent-interface-12345")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
_ = os.Unsetenv("SERVER_INTERFACE")
}()
cfg, err := Load("")
require.NoError(t, err) // Loading should succeed
_, err = cfg.GetBindAddress()
require.Error(t, err)
require.Contains(t, err.Error(), "failed to find interface")
}
func TestConfig_DefaultBindAddress(t *testing.T) {
_ = os.Setenv("DATABASE_URL", "sqlite:test.db")
_ = os.Setenv("JWT_SECRET", "secret")
_ = os.Setenv("SERVER_PORT", "8080")
defer func() {
_ = os.Unsetenv("DATABASE_URL")
_ = os.Unsetenv("JWT_SECRET")
_ = os.Unsetenv("SERVER_PORT")
}()
cfg, err := Load("")
require.NoError(t, err)
addr, err := cfg.GetBindAddress()
require.NoError(t, err)
require.Equal(t, ":8080", addr) // Default: bind to all interfaces
url, err := cfg.GetServerURL()
require.NoError(t, err)
require.Equal(t, "http://localhost:8080", url) // Default URL uses localhost
}