opendtu-logger/main_test.go

566 lines
14 KiB
Go

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