package main import ( "encoding/json" "log/slog" "os" "testing" "time" ) func TestBasicAuth(t *testing.T) { tests := []struct { name string username string password string expected string }{ { name: "basic auth test", username: "testuser", password: "testpass", expected: "Basic dGVzdHVzZXI6dGVzdHBhc3M=", }, { name: "empty credentials", username: "", password: "", expected: "Basic Og==", }, { name: "special characters", username: "user@domain.com", password: "p@ssw0rd!", expected: "Basic dXNlckBkb21haW4uY29tOnBAc3N3MHJkIQ==", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := basicAuth(tt.username, tt.password) if result != tt.expected { t.Errorf("basicAuth() = %v, want %v", result, tt.expected) } }) } } func TestGetLogLevel(t *testing.T) { tests := []struct { name string logLevel string defaultLevel slog.Level expected slog.Level }{ { name: "DEBUG level", logLevel: "DEBUG", defaultLevel: slog.LevelInfo, expected: slog.LevelDebug, }, { name: "INFO level", logLevel: "INFO", defaultLevel: slog.LevelDebug, expected: slog.LevelInfo, }, { name: "WARN level", logLevel: "WARN", defaultLevel: slog.LevelDebug, expected: slog.LevelWarn, }, { name: "ERROR level", logLevel: "ERROR", defaultLevel: slog.LevelDebug, expected: slog.LevelError, }, { name: "invalid level returns default", logLevel: "INVALID", defaultLevel: slog.LevelInfo, expected: slog.LevelInfo, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Temporarily set the config.LogLevel originalLogLevel := config.LogLevel config.LogLevel = tt.logLevel defer func() { config.LogLevel = originalLogLevel }() result := getLogLevel(tt.defaultLevel) if result != tt.expected { t.Errorf("getLogLevel() = %v, want %v", result, tt.expected) } }) } } func TestVUDStruct(t *testing.T) { jsonData := `{"v": 123.45, "u": "W", "d": 2}` var vud VUD err := json.Unmarshal([]byte(jsonData), &vud) if err != nil { t.Fatalf("Failed to unmarshal VUD: %v", err) } if vud.V != 123.45 { t.Errorf("Expected V=123.45, got %v", vud.V) } if vud.U != "W" { t.Errorf("Expected U=W, got %v", vud.U) } if vud.D != 2 { t.Errorf("Expected D=2, got %v", vud.D) } } func TestInverterStruct(t *testing.T) { jsonData := `{ "serial": "123456789", "name": "Test Inverter", "producing": true, "limit_relative": 80.5, "limit_absolute": 800.0, "AC": { "0": { "Power": {"v": 250.5, "u": "W", "d": 1}, "Voltage": {"v": 230.0, "u": "V", "d": 1}, "Current": {"v": 1.09, "u": "A", "d": 2}, "Frequency": {"v": 50.0, "u": "Hz", "d": 1}, "PowerFactor": {"v": 1.0, "u": "", "d": 2}, "ReactivePower": {"v": 0.0, "u": "var", "d": 1} } }, "DC": { "0": { "Name": {"u": "String 1"}, "Power": {"v": 260.0, "u": "W", "d": 1}, "Voltage": {"v": 35.2, "u": "V", "d": 1}, "Current": {"v": 7.4, "u": "A", "d": 1}, "YieldDay": {"v": 2.5, "u": "kWh", "d": 3}, "YieldTotal": {"v": 1250.8, "u": "kWh", "d": 3}, "Irradiation": {"v": 85.2, "u": "%", "d": 1, "max": 100} } }, "events": 2, "poll_enabled": true, "reachable": true, "data_age": 0, "INV": { "0": { "Temperature": {"v": 32.5, "u": "°C", "d": 1}, "Efficiency": {"v": 96.2, "u": "%", "d": 1}, "Power DC": {"v": 260.0, "u": "W", "d": 1}, "YieldDay": {"v": 2.5, "u": "kWh", "d": 3}, "YieldTotal": {"v": 1250.8, "u": "kWh", "d": 3} } } }` var inverter Inverter err := json.Unmarshal([]byte(jsonData), &inverter) if err != nil { t.Fatalf("Failed to unmarshal Inverter: %v", err) } if inverter.Serial != "123456789" { t.Errorf("Expected Serial=123456789, got %v", inverter.Serial) } if inverter.Name != "Test Inverter" { t.Errorf("Expected Name=Test Inverter, got %v", inverter.Name) } if !inverter.Producing { t.Errorf("Expected Producing=true, got %v", inverter.Producing) } if inverter.LimitRelative != 80.5 { t.Errorf("Expected LimitRelative=80.5, got %v", inverter.LimitRelative) } // Test AC data if len(inverter.AC) != 1 { t.Errorf("Expected 1 AC entry, got %v", len(inverter.AC)) } if ac, exists := inverter.AC["0"]; exists { if ac.Power.V != 250.5 { t.Errorf("Expected AC Power=250.5, got %v", ac.Power.V) } if ac.Voltage.V != 230.0 { t.Errorf("Expected AC Voltage=230.0, got %v", ac.Voltage.V) } } else { t.Error("Expected AC[0] to exist") } // Test DC data if len(inverter.DC) != 1 { t.Errorf("Expected 1 DC entry, got %v", len(inverter.DC)) } if dc, exists := inverter.DC["0"]; exists { if dc.Name.U != "String 1" { t.Errorf("Expected DC Name=String 1, got %v", dc.Name.U) } if dc.Power.V != 260.0 { t.Errorf("Expected DC Power=260.0, got %v", dc.Power.V) } } else { t.Error("Expected DC[0] to exist") } // Test INV data if len(inverter.INV) != 1 { t.Errorf("Expected 1 INV entry, got %v", len(inverter.INV)) } if inv, exists := inverter.INV["0"]; exists { if inv.Temperature.V != 32.5 { t.Errorf("Expected INV Temperature=32.5, got %v", inv.Temperature.V) } if inv.Efficiency.V != 96.2 { t.Errorf("Expected INV Efficiency=96.2, got %v", inv.Efficiency.V) } } else { t.Error("Expected INV[0] to exist") } } func TestEventStruct(t *testing.T) { jsonData := `{ "message_id": 1, "message": "Test event message", "start_time": 1634567890, "end_time": 1634567950 }` var event Event err := json.Unmarshal([]byte(jsonData), &event) if err != nil { t.Fatalf("Failed to unmarshal Event: %v", err) } if event.MessageID != 1 { t.Errorf("Expected MessageID=1, got %v", event.MessageID) } if event.Message != "Test event message" { t.Errorf("Expected Message=Test event message, got %v", event.Message) } if event.StartTime != 1634567890 { t.Errorf("Expected StartTime=1634567890, got %v", event.StartTime) } if event.EndTime != 1634567950 { t.Errorf("Expected EndTime=1634567950, got %v", event.EndTime) } } func TestLiveDataStruct(t *testing.T) { jsonData := `{ "inverters": [ { "serial": "123456789", "name": "Test Inverter", "producing": true, "limit_relative": 80.5, "limit_absolute": 800.0, "AC": {}, "DC": {}, "events": 0, "poll_enabled": true, "reachable": true, "data_age": 0, "INV": {} } ], "total": { "Power": {"v": 250.5, "u": "W", "d": 1}, "YieldDay": {"v": 2.5, "u": "kWh", "d": 3}, "YieldTotal": {"v": 1250.8, "u": "kWh", "d": 3} }, "hints": { "time_sync": true, "radio_problem": false, "default_password": false } }` var liveData LiveData err := json.Unmarshal([]byte(jsonData), &liveData) if err != nil { t.Fatalf("Failed to unmarshal LiveData: %v", err) } if len(liveData.Inverters) != 1 { t.Errorf("Expected 1 inverter, got %v", len(liveData.Inverters)) } if liveData.Total.Power.V != 250.5 { t.Errorf("Expected Total Power=250.5, got %v", liveData.Total.Power.V) } if !liveData.Hints.TimeSync { t.Errorf("Expected Hints.TimeSync=true, got %v", liveData.Hints.TimeSync) } if liveData.Hints.RadioProblem { t.Errorf("Expected Hints.RadioProblem=false, got %v", liveData.Hints.RadioProblem) } } func TestConfigLoadFromEnv(t *testing.T) { // Save original environment variables originalDB := os.Getenv("DB_URL") originalAddress := os.Getenv("OPENDTU_ADDRESS") originalAuth := os.Getenv("OPENDTU_AUTH") originalUser := os.Getenv("OPENDTU_USERNAME") originalPassword := os.Getenv("OPENDTU_PASSWORD") originalTimescale := os.Getenv("TIMESCALEDB_ENABLED") originalTZ := os.Getenv("TZ") originalLogLevel := os.Getenv("LOG_LEVEL") originalConfigFile := os.Getenv("CONFIG_FILE") // Clean up after test defer func() { os.Setenv("DB_URL", originalDB) os.Setenv("OPENDTU_ADDRESS", originalAddress) os.Setenv("OPENDTU_AUTH", originalAuth) os.Setenv("OPENDTU_USERNAME", originalUser) os.Setenv("OPENDTU_PASSWORD", originalPassword) os.Setenv("TIMESCALEDB_ENABLED", originalTimescale) os.Setenv("TZ", originalTZ) os.Setenv("LOG_LEVEL", originalLogLevel) os.Setenv("CONFIG_FILE", originalConfigFile) }() // Set test environment variables os.Setenv("CONFIG_FILE", "/nonexistent/config.json") // Force env var fallback os.Setenv("DB_URL", "postgres://testuser:testpass@localhost/testdb") os.Setenv("OPENDTU_ADDRESS", "192.168.1.100") os.Setenv("OPENDTU_AUTH", "true") os.Setenv("OPENDTU_USERNAME", "admin") os.Setenv("OPENDTU_PASSWORD", "secret") os.Setenv("TIMESCALEDB_ENABLED", "true") os.Setenv("TZ", "Europe/Amsterdam") os.Setenv("LOG_LEVEL", "DEBUG") config, err := loadConfig() if err != nil { t.Fatalf("loadConfig() failed: %v", err) } if config.DB != "postgres://testuser:testpass@localhost/testdb" { t.Errorf("Expected DB from env, got %v", config.DB) } if config.OpenDTUAddress != "192.168.1.100" { t.Errorf("Expected OpenDTUAddress from env, got %v", config.OpenDTUAddress) } if !config.OpenDTUAuth { t.Errorf("Expected OpenDTUAuth=true from env, got %v", config.OpenDTUAuth) } if config.OpenDTUUser != "admin" { t.Errorf("Expected OpenDTUUser from env, got %v", config.OpenDTUUser) } if config.OpenDTUPassword != "secret" { t.Errorf("Expected OpenDTUPassword from env, got %v", config.OpenDTUPassword) } if !config.TimescaleDB { t.Errorf("Expected TimescaleDB=true from env, got %v", config.TimescaleDB) } if config.TZ != "Europe/Amsterdam" { t.Errorf("Expected TZ from env, got %v", config.TZ) } if config.LogLevel != "DEBUG" { t.Errorf("Expected LogLevel from env, got %v", config.LogLevel) } } func TestTimeZoneValidation(t *testing.T) { validTimezones := []string{ "UTC", "Europe/Amsterdam", "America/New_York", "Asia/Tokyo", } for _, tz := range validTimezones { t.Run(tz, func(t *testing.T) { _, err := time.LoadLocation(tz) if err != nil { t.Errorf("Timezone %s should be valid but got error: %v", tz, err) } }) } invalidTimezones := []string{ "Invalid/Timezone", "Not/A/Real/Place", "", } for _, tz := range invalidTimezones { if tz == "" { continue // Empty string is handled differently } t.Run(tz, func(t *testing.T) { _, err := time.LoadLocation(tz) if err == nil { t.Errorf("Timezone %s should be invalid but was accepted", tz) } }) } } // Test RadioStatistics structure (from official OpenDTU) func TestRadioStatisticsStruct(t *testing.T) { jsonData := `{ "tx_request": 12345, "tx_re_request": 234, "rx_success": 12000, "rx_fail_nothing": 50, "rx_fail_partial": 30, "rx_fail_corrupt": 21, "rssi": -65.5 }` var stats RadioStatistics err := json.Unmarshal([]byte(jsonData), &stats) if err != nil { t.Fatalf("Failed to unmarshal RadioStatistics: %v", err) } if stats.TxRequest != 12345 { t.Errorf("Expected TxRequest=12345, got %v", stats.TxRequest) } if stats.TxReRequest != 234 { t.Errorf("Expected TxReRequest=234, got %v", stats.TxReRequest) } if stats.RxSuccess != 12000 { t.Errorf("Expected RxSuccess=12000, got %v", stats.RxSuccess) } if stats.RxFailNothing != 50 { t.Errorf("Expected RxFailNothing=50, got %v", stats.RxFailNothing) } if stats.RxFailPartial != 30 { t.Errorf("Expected RxFailPartial=30, got %v", stats.RxFailPartial) } if stats.RxFailCorrupt != 21 { t.Errorf("Expected RxFailCorrupt=21, got %v", stats.RxFailCorrupt) } if stats.RSSI != -65.5 { t.Errorf("Expected RSSI=-65.5, got %v", stats.RSSI) } } // Test inverter struct with new OpenDTU fields: order, data_age_ms, radio_stats func TestInverterWithOpenDTUFields(t *testing.T) { jsonData := `{ "serial": "114173123456", "name": "Hoymiles HM-800", "order": 0, "data_age": 0, "data_age_ms": 124, "poll_enabled": true, "reachable": true, "producing": true, "limit_relative": 100.0, "limit_absolute": 800.0, "events": 2, "radio_stats": { "tx_request": 12345, "tx_re_request": 234, "rx_success": 12000, "rx_fail_nothing": 50, "rx_fail_partial": 30, "rx_fail_corrupt": 21, "rssi": -65.5 }, "AC": {}, "DC": {}, "INV": {} }` var inverter Inverter err := json.Unmarshal([]byte(jsonData), &inverter) if err != nil { t.Fatalf("Failed to unmarshal Inverter with OpenDTU fields: %v", err) } // Test new fields from official OpenDTU if inverter.Order != 0 { t.Errorf("Expected Order=0, got %v", inverter.Order) } if inverter.DataAge != 0 { t.Errorf("Expected DataAge=0, got %v", inverter.DataAge) } if inverter.DataAgeMs != 124 { t.Errorf("Expected DataAgeMs=124, got %v", inverter.DataAgeMs) } // Test radio_stats if inverter.RadioStats.TxRequest != 12345 { t.Errorf("Expected RadioStats.TxRequest=12345, got %v", inverter.RadioStats.TxRequest) } if inverter.RadioStats.RxSuccess != 12000 { t.Errorf("Expected RadioStats.RxSuccess=12000, got %v", inverter.RadioStats.RxSuccess) } if inverter.RadioStats.RSSI != -65.5 { t.Errorf("Expected RadioStats.RSSI=-65.5, got %v", inverter.RadioStats.RSSI) } // Test original fields still work if inverter.Serial != "114173123456" { t.Errorf("Expected Serial=114173123456, got %v", inverter.Serial) } if inverter.Name != "Hoymiles HM-800" { t.Errorf("Expected Name=Hoymiles HM-800, got %v", inverter.Name) } if !inverter.Producing { t.Errorf("Expected Producing=true, got %v", inverter.Producing) } } // Test Hints struct with new pin_mapping_issue field from OpenDTU func TestHintsWithPinMappingIssue(t *testing.T) { jsonData := `{ "time_sync": true, "radio_problem": false, "default_password": false, "pin_mapping_issue": false }` var hints Hints err := json.Unmarshal([]byte(jsonData), &hints) if err != nil { t.Fatalf("Failed to unmarshal Hints with pin_mapping_issue: %v", err) } if !hints.TimeSync { t.Errorf("Expected TimeSync=true, got %v", hints.TimeSync) } if hints.RadioProblem { t.Errorf("Expected RadioProblem=false, got %v", hints.RadioProblem) } if hints.DefaultPassword { t.Errorf("Expected DefaultPassword=false, got %v", hints.DefaultPassword) } if hints.PinMappingIssue { t.Errorf("Expected PinMappingIssue=false, got %v", hints.PinMappingIssue) } // Test when pin_mapping_issue is true jsonDataWithIssue := `{ "time_sync": true, "radio_problem": false, "default_password": false, "pin_mapping_issue": true }` err = json.Unmarshal([]byte(jsonDataWithIssue), &hints) if err != nil { t.Fatalf("Failed to unmarshal Hints with pin_mapping_issue=true: %v", err) } if !hints.PinMappingIssue { t.Errorf("Expected PinMappingIssue=true, got %v", hints.PinMappingIssue) } }