436 lines
12 KiB
Go
436 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
)
|
|
|
|
func TestCreateLoggerWithLevel(t *testing.T) {
|
|
logger := createLoggerWithLevel(slog.LevelWarn)
|
|
if logger == nil {
|
|
t.Fatal("expected logger instance")
|
|
}
|
|
|
|
handler := logger.Handler()
|
|
if handler.Enabled(context.Background(), slog.LevelInfo) {
|
|
t.Fatalf("expected info level to be disabled for warn handler")
|
|
}
|
|
if !handler.Enabled(context.Background(), slog.LevelError) {
|
|
t.Fatalf("expected error level to be enabled for warn handler")
|
|
}
|
|
}
|
|
|
|
func TestInsertLiveDataInsertsAllTables(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
config = Config{TZ: "UTC"}
|
|
|
|
inverter := Inverter{
|
|
Serial: "INV01",
|
|
Name: "Dummy Inverter",
|
|
Producing: true,
|
|
LimitRelative: 90.5,
|
|
LimitAbsolute: 1500,
|
|
AC: map[string]InverterAC{
|
|
"0": {
|
|
Power: VUD{V: 123.4},
|
|
Voltage: VUD{V: 230.0},
|
|
Current: VUD{V: 0.53},
|
|
Frequency: VUD{V: 50.0},
|
|
PowerFactor: VUD{V: 0.99},
|
|
ReactivePower: VUD{
|
|
V: 1.1,
|
|
},
|
|
},
|
|
},
|
|
DC: map[string]InverterDC{
|
|
"0": {
|
|
Name: struct {
|
|
U string `json:"u"`
|
|
}{U: "Dummy String"},
|
|
Power: VUD{V: 111.1},
|
|
Voltage: VUD{V: 36.5},
|
|
Current: VUD{V: 3.05},
|
|
YieldDay: VUD{V: 12.0},
|
|
YieldTotal: VUD{V: 456.7},
|
|
Irradiation: struct {
|
|
V float64 `json:"v"`
|
|
U string `json:"u"`
|
|
D int `json:"d"`
|
|
Max int `json:"max"`
|
|
}{V: 75.0, Max: 440},
|
|
},
|
|
},
|
|
INV: map[string]InverterINV{
|
|
"0": {
|
|
Temperature: VUD{V: 40.0},
|
|
Efficiency: VUD{V: 97.5},
|
|
PowerDC: VUD{V: 222.2},
|
|
YieldDay: VUD{V: 15.0},
|
|
YieldTotal: VUD{V: 789.0},
|
|
},
|
|
},
|
|
}
|
|
|
|
total := Total{
|
|
Power: VUD{V: 321.0},
|
|
YieldDay: VUD{V: 25.0},
|
|
YieldTotal: VUD{
|
|
V: 12345.6,
|
|
},
|
|
}
|
|
|
|
hints := Hints{
|
|
TimeSync: true,
|
|
RadioProblem: true,
|
|
DefaultPassword: false,
|
|
PinMappingIssue: true,
|
|
}
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_log").
|
|
WithArgs(sqlmock.AnyArg(), total.Power.V, total.YieldDay.V, total.YieldTotal.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, inverter.Name, inverter.Producing, inverter.LimitRelative, inverter.LimitAbsolute).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters_ac").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, "0", inverter.AC["0"].Power.V, inverter.AC["0"].Voltage.V, inverter.AC["0"].Current.V, inverter.AC["0"].Frequency.V, inverter.AC["0"].PowerFactor.V, inverter.AC["0"].ReactivePower.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters_dc").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, "0", inverter.DC["0"].Name.U, inverter.DC["0"].Power.V, inverter.DC["0"].Voltage.V, inverter.DC["0"].Current.V, inverter.DC["0"].YieldDay.V, inverter.DC["0"].YieldTotal.V, inverter.DC["0"].Irradiation.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters_inv").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, inverter.INV["0"].Temperature.V, inverter.INV["0"].Efficiency.V, inverter.INV["0"].PowerDC.V, inverter.INV["0"].YieldDay.V, inverter.INV["0"].YieldTotal.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_hints").
|
|
WithArgs(sqlmock.AnyArg(), hints.TimeSync, hints.RadioProblem, hints.DefaultPassword, hints.PinMappingIssue).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
insertLiveData(db, inverter, total, hints)
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("not all expectations were met: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetPreviousEventsCount(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
rows := sqlmock.NewRows([]string{"count"}).AddRow(3)
|
|
mock.ExpectQuery("SELECT COUNT\\(\\*\\)").
|
|
WithArgs("INV01").
|
|
WillReturnRows(rows)
|
|
|
|
count := getPreviousEventsCount(db, "INV01")
|
|
if count != 3 {
|
|
t.Fatalf("expected count 3, got %d", count)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("not all expectations were met: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInsertEventsPersistsRows(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
config = Config{TZ: "UTC"}
|
|
|
|
events := &EventsResponse{
|
|
Events: []Event{
|
|
{
|
|
MessageID: 10,
|
|
Message: "Test event",
|
|
StartTime: 111,
|
|
EndTime: 222,
|
|
},
|
|
},
|
|
}
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_events").
|
|
WithArgs(sqlmock.AnyArg(), "INV01", events.Events[0].MessageID, events.Events[0].Message, events.Events[0].StartTime, events.Events[0].EndTime).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
insertEvents(db, "INV01", events)
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("not all expectations were met: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateEventsUpdatesRows(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
events := &EventsResponse{
|
|
Events: []Event{
|
|
{
|
|
StartTime: 100,
|
|
EndTime: 200,
|
|
},
|
|
{
|
|
StartTime: 300,
|
|
EndTime: 400,
|
|
},
|
|
},
|
|
}
|
|
|
|
mock.ExpectExec("UPDATE opendtu_events SET end_time").
|
|
WithArgs(events.Events[0].EndTime, "INV01", events.Events[0].StartTime).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
mock.ExpectExec("UPDATE opendtu_events SET end_time").
|
|
WithArgs(events.Events[1].EndTime, "INV01", events.Events[1].StartTime).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
updateEvents(db, "INV01", events)
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("not all expectations were met: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandleMessageRecordsDataAndEvents(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
eventsResponse := EventsResponse{
|
|
Count: 1,
|
|
Events: []Event{{MessageID: 1, Message: "Test event", StartTime: 50, EndTime: 60}},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/eventlog/status" {
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if got := r.URL.Query().Get("inv"); got != "INV01" {
|
|
t.Fatalf("unexpected inverter query: %s", got)
|
|
}
|
|
if err := json.NewEncoder(w).Encode(eventsResponse); err != nil {
|
|
t.Fatalf("failed to write response: %v", err)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
previousConfig := config
|
|
defer func() { config = previousConfig }()
|
|
|
|
address := strings.TrimPrefix(server.URL, "http://")
|
|
address = strings.TrimPrefix(address, "https://")
|
|
config = Config{
|
|
TZ: "UTC",
|
|
OpenDTUAddress: address,
|
|
}
|
|
|
|
rows := sqlmock.NewRows([]string{"count"}).AddRow(1)
|
|
mock.ExpectQuery("SELECT COUNT\\(\\*\\)").
|
|
WithArgs("INV01").
|
|
WillReturnRows(rows)
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_events").
|
|
WithArgs(sqlmock.AnyArg(), "INV01", eventsResponse.Events[0].MessageID, eventsResponse.Events[0].Message, eventsResponse.Events[0].StartTime, eventsResponse.Events[0].EndTime).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
inverter := Inverter{
|
|
Serial: "INV01",
|
|
Name: "Dummy Inverter",
|
|
Producing: true,
|
|
Reachable: true,
|
|
DataAge: 0,
|
|
Events: 2,
|
|
LimitRelative: 90.5,
|
|
LimitAbsolute: 1500,
|
|
AC: map[string]InverterAC{
|
|
"0": {
|
|
Power: VUD{V: 123.4},
|
|
Voltage: VUD{V: 230.0},
|
|
Current: VUD{V: 0.53},
|
|
Frequency: VUD{V: 50.0},
|
|
PowerFactor: VUD{V: 0.99},
|
|
ReactivePower: VUD{V: 1.1},
|
|
},
|
|
},
|
|
DC: map[string]InverterDC{
|
|
"0": {
|
|
Name: struct {
|
|
U string `json:"u"`
|
|
}{U: "Dummy String"},
|
|
Power: VUD{V: 111.1},
|
|
Voltage: VUD{V: 36.5},
|
|
Current: VUD{V: 3.05},
|
|
YieldDay: VUD{V: 12.0},
|
|
YieldTotal: VUD{V: 456.7},
|
|
Irradiation: struct {
|
|
V float64 `json:"v"`
|
|
U string `json:"u"`
|
|
D int `json:"d"`
|
|
Max int `json:"max"`
|
|
}{V: 75.0, Max: 440},
|
|
},
|
|
},
|
|
INV: map[string]InverterINV{
|
|
"0": {
|
|
Temperature: VUD{V: 40.0},
|
|
Efficiency: VUD{V: 97.5},
|
|
PowerDC: VUD{V: 222.2},
|
|
YieldDay: VUD{V: 15.0},
|
|
YieldTotal: VUD{V: 789.0},
|
|
},
|
|
},
|
|
}
|
|
|
|
total := Total{
|
|
Power: VUD{V: 321.0},
|
|
YieldDay: VUD{V: 25.0},
|
|
YieldTotal: VUD{
|
|
V: 12345.6,
|
|
},
|
|
}
|
|
|
|
hints := Hints{
|
|
TimeSync: true,
|
|
RadioProblem: true,
|
|
DefaultPassword: false,
|
|
PinMappingIssue: true,
|
|
}
|
|
|
|
mock.ExpectExec("INSERT INTO opendtu_log").
|
|
WithArgs(sqlmock.AnyArg(), total.Power.V, total.YieldDay.V, total.YieldTotal.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, inverter.Name, inverter.Producing, inverter.LimitRelative, inverter.LimitAbsolute).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters_ac").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, "0", inverter.AC["0"].Power.V, inverter.AC["0"].Voltage.V, inverter.AC["0"].Current.V, inverter.AC["0"].Frequency.V, inverter.AC["0"].PowerFactor.V, inverter.AC["0"].ReactivePower.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters_dc").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, "0", inverter.DC["0"].Name.U, inverter.DC["0"].Power.V, inverter.DC["0"].Voltage.V, inverter.DC["0"].Current.V, inverter.DC["0"].YieldDay.V, inverter.DC["0"].YieldTotal.V, inverter.DC["0"].Irradiation.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("INSERT INTO opendtu_inverters_inv").
|
|
WithArgs(sqlmock.AnyArg(), inverter.Serial, inverter.INV["0"].Temperature.V, inverter.INV["0"].Efficiency.V, inverter.INV["0"].PowerDC.V, inverter.INV["0"].YieldDay.V, inverter.INV["0"].YieldTotal.V).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("INSERT INTO opendtu_hints").
|
|
WithArgs(sqlmock.AnyArg(), hints.TimeSync, hints.RadioProblem, hints.DefaultPassword, hints.PinMappingIssue).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
liveData := LiveData{
|
|
Inverters: []Inverter{inverter},
|
|
Total: total,
|
|
Hints: hints,
|
|
}
|
|
|
|
payload, err := json.Marshal(liveData)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal live data: %v", err)
|
|
}
|
|
|
|
handleMessage(payload, db)
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("not all expectations were met: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandleMessageSkipsWhenDataStale(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
liveData := LiveData{
|
|
Inverters: []Inverter{
|
|
{
|
|
Serial: "INV01",
|
|
Reachable: false,
|
|
DataAge: 10,
|
|
Events: 2,
|
|
},
|
|
},
|
|
}
|
|
|
|
payload, err := json.Marshal(liveData)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal live data: %v", err)
|
|
}
|
|
|
|
handleMessage(payload, db)
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("unexpected database calls: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnableTimescaleHypertablesExecutesStatements(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
mock.ExpectExec("(?s).*create_hypertable\\('opendtu_log'").
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
if err := enableTimescaleHypertables(db); err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("not all expectations were met: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnableTimescaleHypertablesPropagatesError(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
mock.ExpectExec("(?s).*create_hypertable\\('opendtu_log'").
|
|
WillReturnError(errors.New("boom"))
|
|
|
|
err = enableTimescaleHypertables(db)
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "boom") {
|
|
t.Fatalf("expected wrapped boom error, got %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("expectations mismatch: %v", err)
|
|
}
|
|
}
|