opendtu-logger/integration_test.go

466 lines
14 KiB
Go

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)
}
})
}
}