mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
338 lines
12 KiB
Go
338 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
|
)
|
|
|
|
func TestAvailabilityHandlersCRUDPersistsTargets(t *testing.T) {
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
handler := NewAvailabilityHandlers(
|
|
func(_ context.Context) *config.ConfigPersistence { return persistence },
|
|
nil,
|
|
)
|
|
|
|
createBody := availabilityRequestBody(t, config.AvailabilityTarget{
|
|
Name: "Energy monitor",
|
|
Address: "device.local",
|
|
Protocol: config.AvailabilityProbeICMP,
|
|
Enabled: true,
|
|
PollIntervalSecs: 30,
|
|
TimeoutMillis: 1000,
|
|
FailureThreshold: 2,
|
|
})
|
|
createReq := httptest.NewRequest(http.MethodPost, "/api/availability-targets", createBody)
|
|
createRec := httptest.NewRecorder()
|
|
handler.HandleAdd(createRec, createReq)
|
|
if createRec.Code != http.StatusCreated {
|
|
t.Fatalf("HandleAdd status = %d, body=%s", createRec.Code, createRec.Body.String())
|
|
}
|
|
|
|
var created config.AvailabilityTarget
|
|
if err := json.NewDecoder(createRec.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode created target: %v", err)
|
|
}
|
|
if created.ID == "" {
|
|
t.Fatal("created ID is empty")
|
|
}
|
|
|
|
updated := created
|
|
updated.Enabled = false
|
|
updateBody := availabilityRequestBody(t, updated)
|
|
updateReq := httptest.NewRequest(http.MethodPut, "/api/availability-targets/"+created.ID, updateBody)
|
|
updateRec := httptest.NewRecorder()
|
|
handler.HandleUpdate(updateRec, updateReq)
|
|
if updateRec.Code != http.StatusOK {
|
|
t.Fatalf("HandleUpdate status = %d, body=%s", updateRec.Code, updateRec.Body.String())
|
|
}
|
|
|
|
loaded, err := persistence.LoadAvailabilityTargets()
|
|
if err != nil {
|
|
t.Fatalf("LoadAvailabilityTargets() error = %v", err)
|
|
}
|
|
if len(loaded) != 1 || loaded[0].Enabled {
|
|
t.Fatalf("loaded targets = %+v, want one paused target", loaded)
|
|
}
|
|
|
|
listReq := httptest.NewRequest(http.MethodGet, "/api/availability-targets", nil)
|
|
listRec := httptest.NewRecorder()
|
|
handler.HandleList(listRec, listReq)
|
|
if listRec.Code != http.StatusOK {
|
|
t.Fatalf("HandleList status = %d, body=%s", listRec.Code, listRec.Body.String())
|
|
}
|
|
|
|
var listed []availabilityTargetResponse
|
|
if err := json.NewDecoder(listRec.Body).Decode(&listed); err != nil {
|
|
t.Fatalf("decode listed targets: %v", err)
|
|
}
|
|
if len(listed) != 1 || listed[0].ID != created.ID {
|
|
t.Fatalf("listed targets = %+v, want created target", listed)
|
|
}
|
|
|
|
deleteReq := httptest.NewRequest(http.MethodDelete, "/api/availability-targets/"+created.ID, nil)
|
|
deleteRec := httptest.NewRecorder()
|
|
handler.HandleDelete(deleteRec, deleteReq)
|
|
if deleteRec.Code != http.StatusOK {
|
|
t.Fatalf("HandleDelete status = %d, body=%s", deleteRec.Code, deleteRec.Body.String())
|
|
}
|
|
loaded, err = persistence.LoadAvailabilityTargets()
|
|
if err != nil {
|
|
t.Fatalf("LoadAvailabilityTargets() after delete error = %v", err)
|
|
}
|
|
if len(loaded) != 0 {
|
|
t.Fatalf("loaded targets after delete = %+v, want none", loaded)
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityHandlersCreateNormalizesPingAlias(t *testing.T) {
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
handler := NewAvailabilityHandlers(
|
|
func(_ context.Context) *config.ConfigPersistence { return persistence },
|
|
nil,
|
|
)
|
|
|
|
createBody := availabilityRequestBody(t, config.AvailabilityTarget{
|
|
Name: "Garage sensor",
|
|
Address: "https://garage-sensor.local/status",
|
|
Protocol: config.AvailabilityProbeProtocol("ping"),
|
|
Enabled: true,
|
|
PollIntervalSecs: 30,
|
|
TimeoutMillis: 1000,
|
|
FailureThreshold: 2,
|
|
})
|
|
createReq := httptest.NewRequest(http.MethodPost, "/api/availability-targets", createBody)
|
|
createRec := httptest.NewRecorder()
|
|
handler.HandleAdd(createRec, createReq)
|
|
if createRec.Code != http.StatusCreated {
|
|
t.Fatalf("HandleAdd status = %d, body=%s", createRec.Code, createRec.Body.String())
|
|
}
|
|
|
|
var created config.AvailabilityTarget
|
|
if err := json.NewDecoder(createRec.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode created target: %v", err)
|
|
}
|
|
if created.Protocol != config.AvailabilityProbeICMP {
|
|
t.Fatalf("created Protocol = %q, want %q", created.Protocol, config.AvailabilityProbeICMP)
|
|
}
|
|
if created.Address != "garage-sensor.local" {
|
|
t.Fatalf("created Address = %q, want garage-sensor.local", created.Address)
|
|
}
|
|
|
|
loaded, err := persistence.LoadAvailabilityTargets()
|
|
if err != nil {
|
|
t.Fatalf("LoadAvailabilityTargets() error = %v", err)
|
|
}
|
|
if len(loaded) != 1 {
|
|
t.Fatalf("LoadAvailabilityTargets() length = %d, want 1", len(loaded))
|
|
}
|
|
if loaded[0].Protocol != config.AvailabilityProbeICMP {
|
|
t.Fatalf("persisted Protocol = %q, want %q", loaded[0].Protocol, config.AvailabilityProbeICMP)
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityHandlersTestSavedTarget(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
|
|
ID: "status-page",
|
|
Name: "Status page",
|
|
Address: server.URL,
|
|
Protocol: config.AvailabilityProbeHTTP,
|
|
Enabled: true,
|
|
TimeoutMillis: 1000,
|
|
})
|
|
if err := persistence.SaveAvailabilityTargets([]config.AvailabilityTarget{target}); err != nil {
|
|
t.Fatalf("SaveAvailabilityTargets() error = %v", err)
|
|
}
|
|
|
|
handler := NewAvailabilityHandlers(
|
|
func(_ context.Context) *config.ConfigPersistence { return persistence },
|
|
nil,
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/availability-targets/status-page/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestSavedConnection(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("HandleTestSavedConnection status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var response availabilityTestResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
|
|
t.Fatalf("decode test response: %v", err)
|
|
}
|
|
if !response.Success {
|
|
t.Fatalf("response = %+v, want success", response)
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityHandlersListReturnsMockTargetsInMockMode(t *testing.T) {
|
|
previous := mock.IsMockEnabled()
|
|
if err := mock.SetEnabled(true); err != nil {
|
|
t.Fatalf("enable mock mode: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = mock.SetEnabled(previous) })
|
|
|
|
handler := NewAvailabilityHandlers(
|
|
func(context.Context) *config.ConfigPersistence {
|
|
t.Fatal("mock availability list should not load persistence")
|
|
return nil
|
|
},
|
|
nil,
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/availability-targets", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleList(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("HandleList status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var listed []availabilityTargetResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&listed); err != nil {
|
|
t.Fatalf("decode listed targets: %v", err)
|
|
}
|
|
if len(listed) < 5 {
|
|
t.Fatalf("expected mock availability targets, got %+v", listed)
|
|
}
|
|
foundMQTT := false
|
|
foundESPHome := false
|
|
for _, target := range listed {
|
|
switch target.ID {
|
|
case "mock-availability-mqtt-meter":
|
|
foundMQTT = true
|
|
if target.Protocol != config.AvailabilityProbeTCP || target.Port != 1883 {
|
|
t.Fatalf("unexpected MQTT target: %+v", target.AvailabilityTarget)
|
|
}
|
|
if target.Status == nil || !target.Status.Available {
|
|
t.Fatalf("expected successful MQTT status, got %+v", target.Status)
|
|
}
|
|
case "mock-availability-esphome-greenhouse":
|
|
foundESPHome = true
|
|
if target.Name != "ESPHome greenhouse sensor" {
|
|
t.Fatalf("unexpected ESPHome target name: %+v", target.AvailabilityTarget)
|
|
}
|
|
if target.Protocol != config.AvailabilityProbeTCP || target.Port != 6053 {
|
|
t.Fatalf("unexpected ESPHome target: %+v", target.AvailabilityTarget)
|
|
}
|
|
if target.Status == nil || !target.Status.Available {
|
|
t.Fatalf("expected successful ESPHome status, got %+v", target.Status)
|
|
}
|
|
}
|
|
}
|
|
if !foundMQTT {
|
|
t.Fatalf("expected MQTT power meter target, got %+v", listed)
|
|
}
|
|
if !foundESPHome {
|
|
t.Fatalf("expected ESPHome greenhouse sensor target, got %+v", listed)
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityHandlersTestSavedMockTargetUsesSyntheticStatus(t *testing.T) {
|
|
previous := mock.IsMockEnabled()
|
|
if err := mock.SetEnabled(true); err != nil {
|
|
t.Fatalf("enable mock mode: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = mock.SetEnabled(previous) })
|
|
|
|
handler := NewAvailabilityHandlers(nil, nil)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/availability-targets/mock-availability-door-controller/test", nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.HandleTestSavedConnection(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("HandleTestSavedConnection status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var response availabilityTestResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
|
|
t.Fatalf("decode test response: %v", err)
|
|
}
|
|
if response.Success {
|
|
t.Fatalf("expected synthetic failure for offline mock target, got %+v", response)
|
|
}
|
|
if response.Error != "icmp probe timed out" {
|
|
t.Fatalf("unexpected mock test error: %+v", response)
|
|
}
|
|
}
|
|
|
|
func availabilityRequestBody(t *testing.T, target config.AvailabilityTarget) *bytes.Reader {
|
|
t.Helper()
|
|
payload, err := json.Marshal(target)
|
|
if err != nil {
|
|
t.Fatalf("marshal target: %v", err)
|
|
}
|
|
return bytes.NewReader(payload)
|
|
}
|
|
|
|
func TestAvailabilityTargetLinkedResourceIDRoundTrip(t *testing.T) {
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
handler := NewAvailabilityHandlers(
|
|
func(_ context.Context) *config.ConfigPersistence { return persistence },
|
|
nil,
|
|
)
|
|
|
|
createBody := availabilityRequestBody(t, config.AvailabilityTarget{
|
|
Name: "Energy monitor",
|
|
Address: "device.local",
|
|
Protocol: config.AvailabilityProbeICMP,
|
|
Enabled: true,
|
|
PollIntervalSecs: 30,
|
|
TimeoutMillis: 1000,
|
|
FailureThreshold: 2,
|
|
LinkedResourceID: "agent-test-123",
|
|
})
|
|
createReq := httptest.NewRequest(http.MethodPost, "/api/availability-targets", createBody)
|
|
createRec := httptest.NewRecorder()
|
|
handler.HandleAdd(createRec, createReq)
|
|
if createRec.Code != http.StatusCreated {
|
|
t.Fatalf("HandleAdd status = %d, body=%s", createRec.Code, createRec.Body.String())
|
|
}
|
|
|
|
var created config.AvailabilityTarget
|
|
if err := json.NewDecoder(createRec.Body).Decode(&created); err != nil {
|
|
t.Fatalf("decode created target: %v", err)
|
|
}
|
|
if created.LinkedResourceID != "agent-test-123" {
|
|
t.Fatalf("LinkedResourceID = %q, want %q", created.LinkedResourceID, "agent-test-123")
|
|
}
|
|
|
|
deleteReq := httptest.NewRequest(http.MethodDelete, "/api/availability-targets/"+created.ID, nil)
|
|
deleteRec := httptest.NewRecorder()
|
|
handler.HandleDelete(deleteRec, deleteReq)
|
|
|
|
createBodyNoID := availabilityRequestBody(t, config.AvailabilityTarget{
|
|
Name: "Energy monitor 2",
|
|
Address: "device2.local",
|
|
Protocol: config.AvailabilityProbeICMP,
|
|
Enabled: true,
|
|
PollIntervalSecs: 30,
|
|
TimeoutMillis: 1000,
|
|
FailureThreshold: 2,
|
|
})
|
|
createReqNoID := httptest.NewRequest(http.MethodPost, "/api/availability-targets", createBodyNoID)
|
|
createRecNoID := httptest.NewRecorder()
|
|
handler.HandleAdd(createRecNoID, createReqNoID)
|
|
if createRecNoID.Code != http.StatusCreated {
|
|
t.Fatalf("HandleAdd status = %d, body=%s", createRecNoID.Code, createRecNoID.Body.String())
|
|
}
|
|
|
|
var createdNoID config.AvailabilityTarget
|
|
if err := json.NewDecoder(createRecNoID.Body).Decode(&createdNoID); err != nil {
|
|
t.Fatalf("decode created target: %v", err)
|
|
}
|
|
if createdNoID.LinkedResourceID != "" {
|
|
t.Fatalf("LinkedResourceID = %q, want empty string", createdNoID.LinkedResourceID)
|
|
}
|
|
}
|