package server import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "testing/fstest" "finance-duck/internal/app" ) func TestOriginAndHostGuardProtectNoLoginService(t *testing.T) { t.Setenv("OPENROUTER_API_KEY", "") t.Setenv("ENABLEBANKING_APP_ID", "") t.Setenv("ENABLEBANKING_KEY_FILE", "") t.Setenv("ENABLEBANKING_REDIRECT_URL", "") a, err := app.Open(t.TempDir()) if err != nil { t.Fatal(err) } defer a.Close() h, err := New(a, fstest.MapFS{"index.html": &fstest.MapFile{Data: []byte("Finance")}}, "") if err != nil { t.Fatal(err) } cases := []struct { name, host, origin, content string want int }{{"rebound host", "attacker.example", "", "application/json", 403}, {"cross origin", "localhost:8080", "https://attacker.example", "application/json", 403}, {"simple form CSRF", "localhost:8080", "", "text/plain", 415}, {"valid local mutation", "localhost:8080", "http://localhost:8080", "application/json", 200}} for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/settings", strings.NewReader(`{"model":"example/model","include_amount":false}`)) r.Host = tt.host r.Header.Set("Content-Type", tt.content) r.Header.Set("Origin", tt.origin) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != tt.want { t.Fatalf("got %d: %s", w.Code, w.Body.String()) } }) } r := httptest.NewRequest(http.MethodGet, "http://localhost:8080/api/state", nil) w := httptest.NewRecorder() h.ServeHTTP(w, r) var s app.State if err = json.NewDecoder(w.Body).Decode(&s); err != nil { t.Fatal(err) } if s.Settings.Model != "example/model" { t.Fatal("same-origin edit not persisted") } r = httptest.NewRequest(http.MethodGet, "http://localhost:8080/", nil) w = httptest.NewRecorder() h.ServeHTTP(w, r) b, _ := io.ReadAll(w.Body) if w.Code != 200 || !strings.Contains(string(b), "") { t.Fatalf("UI not served: %d %s", w.Code, b) } }