package main import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" ) func loadTestData(filename string) ([]byte, error) { return os.ReadFile(filepath.Join("testdata", filename)) } func TestLiveDataFromFile(t *testing.T) { // Test producing data data, err := loadTestData("livedata_producing.json") if err != nil { t.Fatalf("Failed to load test data: %v", err) } var liveData LiveData err = json.Unmarshal(data, &liveData) if err != nil { t.Fatalf("Failed to unmarshal LiveData: %v", err) } // Validate the structure if len(liveData.Inverters) != 2 { t.Errorf("Expected 2 inverters, got %v", len(liveData.Inverters)) } inverter := liveData.Inverters[0] if inverter.Serial != "114173123456" { t.Errorf("Expected first inverter serial 114173123456, got %v", inverter.Serial) } if !inverter.Producing { t.Errorf("Expected first inverter to be producing") } if !inverter.Reachable { t.Errorf("Expected first inverter to be reachable") } // Test new OpenDTU fields if inverter.Order != 0 { t.Errorf("Expected first inverter Order=0, got %v", inverter.Order) } if inverter.DataAge != 0 { t.Errorf("Expected first inverter DataAge=0, got %v", inverter.DataAge) } if inverter.DataAgeMs != 124 { t.Errorf("Expected first inverter DataAgeMs=124, got %v", inverter.DataAgeMs) } // Test radio_stats from OpenDTU 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 second inverter has correct order if liveData.Inverters[1].Order != 1 { t.Errorf("Expected second inverter Order=1, got %v", liveData.Inverters[1].Order) } // Test Hints with pin_mapping_issue 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) } if liveData.Hints.PinMappingIssue { t.Errorf("Expected Hints.PinMappingIssue=false, got %v", liveData.Hints.PinMappingIssue) } // Check total power if liveData.Total.Power.V != 1276.9 { t.Errorf("Expected total power 1276.9W, got %v", liveData.Total.Power.V) } // Test night data data, err = loadTestData("livedata_night.json") if err != nil { t.Fatalf("Failed to load night test data: %v", err) } err = json.Unmarshal(data, &liveData) if err != nil { t.Fatalf("Failed to unmarshal night LiveData: %v", err) } if len(liveData.Inverters) != 1 { t.Errorf("Expected 1 inverter in night data, got %v", len(liveData.Inverters)) } inverter = liveData.Inverters[0] if inverter.Producing { t.Errorf("Expected inverter not to be producing at night") } if liveData.Total.Power.V != 0.0 { t.Errorf("Expected total power 0W at night, got %v", liveData.Total.Power.V) } if liveData.Total.YieldTotal.V != 16253.99 { t.Errorf("Expected total YieldTotal 16253.99kWh, got %v", liveData.Total.YieldTotal.V) } // Test that DataAge is old at night (no recent data) if inverter.DataAge != 20840 { t.Errorf("Expected DataAge=20840 (old data at night), got %v", inverter.DataAge) } if inverter.DataAgeMs != 20840477 { t.Errorf("Expected DataAgeMs=20840477, got %v", inverter.DataAgeMs) } if inverter.LimitAbsolute != 2250 { t.Errorf("Expected LimitAbsolute=2250, got %v", inverter.LimitAbsolute) } if inverter.PollEnabled { t.Errorf("Expected PollEnabled=false at night") } if inverter.Reachable { t.Errorf("Expected Reachable=false at night") } if len(inverter.DC) != 6 { t.Errorf("Expected 6 DC strings, got %d", len(inverter.DC)) } if inverter.DC["5"].Irradiation.Max != 440 { t.Errorf("Expected Irradiation max 440 for string 5, got %d", inverter.DC["5"].Irradiation.Max) } if inverter.RadioStats.TxRequest != 147 { t.Errorf("Expected TxRequest=147, got %v", inverter.RadioStats.TxRequest) } if inverter.RadioStats.RxFailNothing != 147 { t.Errorf("Expected RxFailNothing=147, got %v", inverter.RadioStats.RxFailNothing) } if inverter.RadioStats.RSSI != -62 { t.Errorf("Expected RSSI=-62, got %v", inverter.RadioStats.RSSI) } // Test warning data with limit fallback and hints data, err = loadTestData("livedata_warnings.json") if err != nil { t.Fatalf("Failed to load warning test data: %v", err) } err = json.Unmarshal(data, &liveData) if err != nil { t.Fatalf("Failed to unmarshal warning LiveData: %v", err) } if len(liveData.Inverters) != 1 { t.Fatalf("Expected 1 inverter in warning data, got %d", len(liveData.Inverters)) } inverter = liveData.Inverters[0] if inverter.LimitAbsolute != -1 { t.Errorf("Expected LimitAbsolute=-1 fallback, got %v", inverter.LimitAbsolute) } if inverter.Events != -1 { t.Errorf("Expected Events=-1 when event log missing, got %v", inverter.Events) } if !inverter.PollEnabled { t.Errorf("Expected PollEnabled=true in warning data") } if inverter.Reachable { t.Errorf("Expected Reachable=false in warning data") } if inverter.RadioStats.RxFailNothing != 12 { t.Errorf("Expected RxFailNothing=12, got %v", inverter.RadioStats.RxFailNothing) } if liveData.Hints.TimeSync { t.Errorf("Expected Hints.TimeSync=false, got %v", liveData.Hints.TimeSync) } if !liveData.Hints.RadioProblem { t.Errorf("Expected Hints.RadioProblem=true, got %v", liveData.Hints.RadioProblem) } if !liveData.Hints.DefaultPassword { t.Errorf("Expected Hints.DefaultPassword=true, got %v", liveData.Hints.DefaultPassword) } if !liveData.Hints.PinMappingIssue { t.Errorf("Expected Hints.PinMappingIssue=true, got %v", liveData.Hints.PinMappingIssue) } var generic map[string]interface{} if err := json.Unmarshal(data, &generic); err != nil { t.Fatalf("Failed to unmarshal warning fixture generically: %v", err) } invertersValue, ok := generic["inverters"].([]interface{}) if !ok || len(invertersValue) == 0 { t.Fatalf("Generic inverter array missing") } invMap, ok := invertersValue[0].(map[string]interface{}) if !ok { t.Fatalf("Generic inverter object missing") } if _, exists := invMap["BAT"]; !exists { t.Errorf("Expected additional channel map 'BAT' to be present in fixture") } } func TestEventsResponseFromFile(t *testing.T) { data, err := loadTestData("events_response.json") if err != nil { t.Fatalf("Failed to load events test data: %v", err) } var eventsResponse EventsResponse err = json.Unmarshal(data, &eventsResponse) if err != nil { t.Fatalf("Failed to unmarshal EventsResponse: %v", err) } if eventsResponse.Count != 3 { t.Errorf("Expected 3 events, got %v", eventsResponse.Count) } if len(eventsResponse.Events) != 3 { t.Errorf("Expected 3 events in array, got %v", len(eventsResponse.Events)) } // Check first event firstEvent := eventsResponse.Events[0] if firstEvent.MessageID != 1 { t.Errorf("Expected first event MessageID 1, got %v", firstEvent.MessageID) } if firstEvent.Message != "Inverter start" { t.Errorf("Expected first event message 'Inverter start', got %v", firstEvent.Message) } if firstEvent.StartTime != 1634567890 { t.Errorf("Expected first event start time 1634567890, got %v", firstEvent.StartTime) } if firstEvent.EndTime != 1634567950 { t.Errorf("Expected first event end time 1634567950, got %v", firstEvent.EndTime) } // Check ongoing event (end_time = 0) ongoingEvent := eventsResponse.Events[2] if ongoingEvent.EndTime != 0 { t.Errorf("Expected ongoing event end time 0, got %v", ongoingEvent.EndTime) } } func TestConfigFromJSONFile(t *testing.T) { // Test config with auth data, err := loadTestData("test_config.json") if err != nil { t.Fatalf("Failed to load config test data: %v", err) } var config Config err = json.Unmarshal(data, &config) if err != nil { t.Fatalf("Failed to unmarshal Config: %v", err) } if config.DB != "postgres://user:password@localhost:5432/opendtu" { t.Errorf("Expected specific DB connection string, got %v", config.DB) } if config.OpenDTUAddress != "192.168.1.100" { t.Errorf("Expected OpenDTU address 192.168.1.100, got %v", config.OpenDTUAddress) } if !config.OpenDTUAuth { t.Errorf("Expected OpenDTU auth to be enabled") } if config.OpenDTUUser != "admin" { t.Errorf("Expected OpenDTU user 'admin', got %v", config.OpenDTUUser) } if !config.TimescaleDB { t.Errorf("Expected TimescaleDB to be enabled") } if config.TZ != "Europe/Amsterdam" { t.Errorf("Expected timezone Europe/Amsterdam, got %v", config.TZ) } if config.LogLevel != "INFO" { t.Errorf("Expected log level INFO, got %v", config.LogLevel) } } func TestQueryEventsEndpointMock(t *testing.T) { // Create test data testResponse := EventsResponse{ Count: 2, Events: []Event{ { MessageID: 1, Message: "Test event 1", StartTime: 1634567890, EndTime: 1634567950, }, { MessageID: 2, Message: "Test event 2", StartTime: 1634568000, EndTime: 0, // Ongoing event }, }, } // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check that the URL contains the expected inverter serial if r.URL.Query().Get("inv") != "123456789" { t.Errorf("Expected inverter serial 123456789, got %v", r.URL.Query().Get("inv")) } // Check the endpoint path if r.URL.Path != "/api/eventlog/status" { t.Errorf("Expected path /api/eventlog/status, got %v", r.URL.Path) } // Return test data w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(testResponse) })) defer server.Close() // Set config to use test server originalAddress := config.OpenDTUAddress config.OpenDTUAddress = server.URL[7:] // Remove "http://" prefix defer func() { config.OpenDTUAddress = originalAddress }() // Test the function response, err := queryEventsEndpoint("123456789") if err != nil { t.Fatalf("queryEventsEndpoint failed: %v", err) } if response.Count != 2 { t.Errorf("Expected 2 events, got %v", response.Count) } if len(response.Events) != 2 { t.Errorf("Expected 2 events in array, got %v", len(response.Events)) } if response.Events[0].Message != "Test event 1" { t.Errorf("Expected first event message 'Test event 1', got %v", response.Events[0].Message) } } func TestQueryEventsEndpointWithAuth(t *testing.T) { // Create a test server that checks auth server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check for Authorization header auth := r.Header.Get("Authorization") expectedAuth := basicAuth("testuser", "testpass") if auth != expectedAuth { t.Errorf("Expected auth header %v, got %v", expectedAuth, auth) w.WriteHeader(http.StatusUnauthorized) return } // Return empty response for this test response := EventsResponse{Count: 0, Events: []Event{}} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) })) defer server.Close() // Set config to use test server with auth originalAddress := config.OpenDTUAddress originalAuth := config.OpenDTUAuth originalUser := config.OpenDTUUser originalPassword := config.OpenDTUPassword config.OpenDTUAddress = server.URL[7:] // Remove "http://" prefix config.OpenDTUAuth = true config.OpenDTUUser = "testuser" config.OpenDTUPassword = "testpass" defer func() { config.OpenDTUAddress = originalAddress config.OpenDTUAuth = originalAuth config.OpenDTUUser = originalUser config.OpenDTUPassword = originalPassword }() // Test the function response, err := queryEventsEndpoint("123456789") if err != nil { t.Fatalf("queryEventsEndpoint with auth failed: %v", err) } if response.Count != 0 { t.Errorf("Expected 0 events, got %v", response.Count) } } func TestQueryEventsEndpointHTTPError(t *testing.T) { // Create a test server that returns an error server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() // Set config to use test server originalAddress := config.OpenDTUAddress config.OpenDTUAddress = server.URL[7:] // Remove "http://" prefix defer func() { config.OpenDTUAddress = originalAddress }() // Test the function _, err := queryEventsEndpoint("123456789") if err == nil { t.Fatalf("Expected error from queryEventsEndpoint, got nil") } } func TestHandleMessageWithLiveData(t *testing.T) { // Load test data data, err := loadTestData("livedata_producing.json") if err != nil { t.Fatalf("Failed to load test data: %v", err) } // Test that handleMessage doesn't crash with valid JSON // Note: This will not actually insert into DB since we don't have a test DB setup // handleMessage(data, nil) // Would panic without proper DB, so we skip this part // Instead, let's test the JSON parsing part var liveData LiveData err = json.Unmarshal(data, &liveData) if err != nil { t.Fatalf("handleMessage would fail due to JSON parsing error: %v", err) } // Verify the conditions that would trigger data recording for _, inverter := range liveData.Inverters { if inverter.DataAge == 0 && inverter.Reachable { // This is the condition for recording data t.Logf("Inverter %s would have data recorded: DataAge=%d, Reachable=%v", inverter.Serial, inverter.DataAge, inverter.Reachable) } if inverter.DataAge == 0 && inverter.Events > 0 { // This is the condition for recording events t.Logf("Inverter %s would have events recorded: DataAge=%d, Events=%d", inverter.Serial, inverter.DataAge, inverter.Events) } } } func TestInvalidJSON(t *testing.T) { invalidJSONs := []string{ `{"invalid": json}`, `{"inverters": [{"serial": }]}`, `{"total": {"Power": {"v": "not a number"}}}`, ``, `null`, } for i, jsonStr := range invalidJSONs { t.Run(fmt.Sprintf("invalid_json_%d", i), func(t *testing.T) { var liveData LiveData err := json.Unmarshal([]byte(jsonStr), &liveData) if err == nil && jsonStr != `` && jsonStr != `null` { t.Errorf("Expected error parsing invalid JSON %q, but got none", jsonStr) } }) } }