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 }