add testing, reverse engineered / generated from OpenDTU source to improve compatibility testing and detecting regressions early.
This commit is contained in:
parent
6b496a39ee
commit
99959726c8
12 changed files with 2888 additions and 0 deletions
436
main_persistence_test.go
Normal file
436
main_persistence_test.go
Normal file
|
@ -0,0 +1,436 @@
|
|||
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)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue