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 TestHealthz(t *testing.T) { s := testServer(t) req := httptest.NewRequest("GET", "/healthz", nil) w := httptest.NewRecorder() s.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("got status %d, want 200", w.Code) } if ct := w.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("got content-type %q, want application/json", ct) } var resp struct { Status string `json:"status"` } json.NewDecoder(w.Body).Decode(&resp) if resp.Status != "ok" { t.Errorf("got status %q, want ok", resp.Status) } } func TestReadyz(t *testing.T) { s := testServer(t) req := httptest.NewRequest("GET", "/readyz", nil) w := httptest.NewRecorder() s.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("got status %d, want 200", w.Code) } if ct := w.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("got content-type %q, want application/json", ct) } var resp struct { Status string `json:"status"` } json.NewDecoder(w.Body).Decode(&resp) if resp.Status != "ok" { t.Errorf("got status %q, want ok", resp.Status) } } func TestBetterventSearch(t *testing.T) { s := testServer(t) req := httptest.NewRequest("GET", "/api/bettervent/search?q=test", nil) w := httptest.NewRecorder() s.Handler().ServeHTTP(w, req) // The test server won't reach bettervent.me, but the handler should still // return valid JSON (either results or a 502 error). if ct := w.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("got content-type %q, want application/json", ct) } } func TestBetterventSearch_EmptyQuery(t *testing.T) { s := testServer(t) req := httptest.NewRequest("GET", "/api/bettervent/search?q=", 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 { Devices []any `json:"devices"` Total int `json:"total"` } json.NewDecoder(w.Body).Decode(&resp) if resp.Total != 0 { t.Errorf("expected 0 results for empty query, got %d", resp.Total) } } func TestBetterventSearch_MethodNotAllowed(t *testing.T) { s := testServer(t) req := httptest.NewRequest("POST", "/api/bettervent/search?q=test", nil) w := httptest.NewRecorder() s.Handler().ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("got status %d, want 405", w.Code) } } 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"]) } }