17ca1f6e6c
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>
150 lines
3.7 KiB
Go
150 lines
3.7 KiB
Go
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
|
|
}
|