466 lines
14 KiB
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)
|
|
}
|
|
})
|
|
}
|
|
}
|