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
This commit is contained in:
167
internal/server/server_test.go
Normal file
167
internal/server/server_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user