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