bcd61ff139
Add SetIssueState client method and handler for toggling issue state
between open and closed via PATCH API. Add AddComment client method
wrapping PostComment. Register new routes POST /issues/{owner}/{repo}/{index}/state
and POST /issues/{owner}/{repo}/{index}/comments. Update issue_detail.html
template with comment form (HTMX inline append) and close/reopen button
(HTMX inline swap of state badge).
Closes leeworks-agents/gitea-mobile#29
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
198 lines
5.2 KiB
Go
198 lines
5.2 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 TestSetIssueState_InvalidState(t *testing.T) {
|
|
h := newTestHandler()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/state", h.SetIssueState)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/issues/org/repo/1/state", 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 TestSetIssueState_InvalidIndex(t *testing.T) {
|
|
h := newTestHandler()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/state", h.SetIssueState)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/issues/org/repo/abc/state", 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 TestAddComment_EmptyBody(t *testing.T) {
|
|
h := newTestHandler()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/comments", h.AddComment)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/issues/org/repo/1/comments", 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
|
|
}
|