Files
HeatGuard/internal/server/server_test.go
vikingowl d5452409b6 feat: rewrite to stateless web app with IndexedDB frontend
Replace CLI + SQLite architecture with a Go web server + vanilla JS
frontend using IndexedDB for all client-side data storage.

- Remove: cli, store, report, static packages
- Add: compute engine (BuildDashboard), server package, web UI
- Add: setup page with CRUD for profiles, rooms, devices, occupants, AC
- Add: dashboard with SVG temperature timeline, risk analysis, care checklist
- Add: i18n support (English/German) with server-side Go templates
- Add: LLM provider selection UI with client-side API key storage
- Add: per-room indoor temperature, edit buttons, language-aware AI summary
2026-02-09 13:31:38 +01:00

168 lines
4.3 KiB
Go

package server
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/cnachtigall/heatwave-autopilot/internal/compute"
"github.com/cnachtigall/heatwave-autopilot/internal/config"
)
func TestMain(m *testing.M) {
// Tests run from the package directory. Set WebFS to point to web/ dir.
WebFS = os.DirFS("../../web")
os.Exit(m.Run())
}
func testServer(t *testing.T) *Server {
t.Helper()
s, err := New(Options{
Port: 0,
DevMode: false,
Config: config.DefaultConfig(),
})
if err != nil {
t.Fatalf("create server: %v", err)
}
return s
}
func TestDashboardPage(t *testing.T) {
s := testServer(t)
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("got status %d, want 200; body: %s", w.Code, w.Body.String())
}
if ct := w.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" {
t.Errorf("got content-type %q, want text/html", ct)
}
}
func TestSetupPage(t *testing.T) {
s := testServer(t)
req := httptest.NewRequest("GET", "/setup", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("got status %d, want 200; body: %s", w.Code, w.Body.String())
}
}
func TestGuidePage(t *testing.T) {
s := testServer(t)
req := httptest.NewRequest("GET", "/guide", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("got status %d, want 200; body: %s", w.Code, w.Body.String())
}
}
func TestNotFound(t *testing.T) {
s := testServer(t)
req := httptest.NewRequest("GET", "/nonexistent", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("got status %d, want 404", w.Code)
}
}
func TestLanguageDetection(t *testing.T) {
s := testServer(t)
req := httptest.NewRequest("GET", "/?lang=de", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("got status %d, want 200", w.Code)
}
body := w.Body.String()
if !bytes.Contains([]byte(body), []byte(`lang="de"`)) {
t.Error("expected lang=de in response")
}
cookies := w.Result().Cookies()
found := false
for _, c := range cookies {
if c.Name == "heatguard_lang" && c.Value == "de" {
found = true
}
}
if !found {
t.Error("expected heatguard_lang cookie to be set")
}
}
func TestComputeAPI_NoForecasts(t *testing.T) {
s := testServer(t)
reqBody := compute.ComputeRequest{
Profile: compute.Profile{Name: "Test", Timezone: "UTC"},
Date: "2025-07-15",
}
b, _ := json.Marshal(reqBody)
req := httptest.NewRequest("POST", "/api/compute/dashboard", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("got status %d, want 400", w.Code)
}
}
func TestComputeAPI_MethodNotAllowed(t *testing.T) {
s := testServer(t)
req := httptest.NewRequest("GET", "/api/compute/dashboard", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("got status %d, want 405", w.Code)
}
}
func TestLLMConfigAPI(t *testing.T) {
s := testServer(t)
req := httptest.NewRequest("GET", "/api/llm/config", nil)
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("got status %d, want 200", w.Code)
}
var resp struct {
Provider string `json:"provider"`
Model string `json:"model"`
Available bool `json:"available"`
}
json.NewDecoder(w.Body).Decode(&resp)
if resp.Available {
t.Errorf("expected available=false for noop provider, got provider=%q model=%q available=%v", resp.Provider, resp.Model, resp.Available)
}
}
func TestLLMSummarize_Noop(t *testing.T) {
s := testServer(t)
body := `{"date":"2025-07-15","peakTempC":35,"riskLevel":"high"}`
req := httptest.NewRequest("POST", "/api/llm/summarize", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
s.Handler().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("got status %d, want 200", w.Code)
}
var resp map[string]string
json.NewDecoder(w.Body).Decode(&resp)
if resp["summary"] != "" {
t.Errorf("expected empty summary for noop, got %q", resp["summary"])
}
}