Files
HeatGuard/internal/server/server_test.go
vikingowl 00c8b0f18c feat: add Kubernetes deployment support with Helm chart
Add health endpoints (/healthz, /readyz), graceful shutdown with
SIGTERM/SIGINT handling, multi-stage Dockerfile with distroless
runtime, and a full Helm chart with security-hardened defaults.
2026-02-10 04:45:14 +01:00

208 lines
5.4 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 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 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"])
}
}