All checks were successful
ci/woodpecker/tag/release Pipeline was successful
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>
332 lines
8.9 KiB
Go
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
|
|
}
|