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
168 lines
4.3 KiB
Go
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"])
|
|
}
|
|
}
|