feat: add HTTP handlers and health endpoint

Implement all HTTP handlers using Go 1.22+ stdlib ServeMux with
HTMX fragment vs full-page response detection.

- internal/handlers/handlers.go: all route handlers
  - GET /health returns 200 for K8s probes
  - GET / dashboard with triage queue from aggregation layer
  - GET /issues lists all issues across orgs
  - GET /pulls lists all PRs across orgs
  - POST /issues creates issue via aggregation layer
  - POST /issues/{owner}/{repo}/{index}/labels assigns labels
  - POST /pulls/{owner}/{repo}/{index}/review submits PR review
  - HX-Request header detection for HTMX fragment vs full page
  - Mobile-first dark theme base layout with bottom navigation
- cmd/server/main.go: refactored to use centralized route registration
- internal/handlers/handlers_test.go: unit tests for health, dashboard,
  HTMX detection, input validation

Closes leeworks-agents/gitea-mobile#4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
agent-company
2026-03-26 04:10:03 +00:00
parent e1e7aa64ca
commit 17ca1f6e6c
3 changed files with 634 additions and 27 deletions
+149
View File
@@ -0,0 +1,149 @@
package handlers
import (
"net/http"
"net/http/httptest"
"testing"
"gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config"
giteaclient "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/gitea"
)
func newTestHandler() *Handler {
cfg := &config.Config{
GiteaURL: "https://gitea.example.com",
SessionSecret: "test-secret-that-is-at-least-32-chars-long",
ListenAddr: ":8080",
}
client := giteaclient.NewClient(cfg.GiteaURL)
return NewHandler(cfg, client)
}
func TestHealth(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
h.Health(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
if body := w.Body.String(); body != "ok\n" {
t.Errorf("body = %q, want %q", body, "ok\n")
}
}
func TestDashboard_NoToken(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
h.Dashboard(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
// Without a token in context, should show "No organizations found."
if body := w.Body.String(); body == "" {
t.Error("expected non-empty response body")
}
}
func TestDashboard_HTMX(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
h.Dashboard(w, req)
// HTMX request should not include full HTML page wrapper.
body := w.Body.String()
if body == "" {
t.Error("expected non-empty response body")
}
// Should NOT contain DOCTYPE for HTMX fragment.
if contains(body, "<!DOCTYPE") {
t.Error("HTMX response should not contain DOCTYPE")
}
}
func TestIsHTMX(t *testing.T) {
tests := []struct {
header string
want bool
}{
{"true", true},
{"false", false},
{"", false},
}
for _, tt := range tests {
req := httptest.NewRequest(http.MethodGet, "/", nil)
if tt.header != "" {
req.Header.Set("HX-Request", tt.header)
}
if got := isHTMX(req); got != tt.want {
t.Errorf("isHTMX(HX-Request=%q) = %v, want %v", tt.header, got, tt.want)
}
}
}
func TestCreateIssue_MissingFields(t *testing.T) {
h := newTestHandler()
req := httptest.NewRequest(http.MethodPost, "/issues", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
h.CreateIssue(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestApplyLabels_InvalidIndex(t *testing.T) {
h := newTestHandler()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
req := httptest.NewRequest(http.MethodPost, "/issues/org/repo/abc/labels", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestSubmitReview_MissingEventType(t *testing.T) {
h := newTestHandler()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/review", h.SubmitReview)
req := httptest.NewRequest(http.MethodPost, "/pulls/org/repo/1/review", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && searchString(s, substr)
}
func searchString(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}