Files
gitea-mobile/internal/handlers/integration_test.go
T
agent-company 2ea20da5ef test: add 43 integration tests for all HTTP handlers
Add comprehensive integration test suite using httptest with a mock
Gitea API server. Tests cover GET and POST handlers for dashboard,
issues, pulls, issue/PR detail, create issue, state changes, comments,
labels, assignees, reviews, and settings. Both regular and HTMX
request paths are tested. Includes TestMain to set working directory
to project root for template loading.

Covers issues: #140 #139 #138 #137 #136 #135 #134 #133 #124 #118
#113 #111 #110

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:12:53 +00:00

1067 lines
31 KiB
Go

package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config"
giteaclient "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/gitea"
"gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/middleware"
)
// TestMain changes the working directory to the project root so that
// template files can be found by handlers that use relative paths.
func TestMain(m *testing.M) {
// Walk up from this source file to find the project root (where go.mod lives).
_, filename, _, _ := runtime.Caller(0)
dir := filepath.Dir(filename)
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
break
}
parent := filepath.Dir(dir)
if parent == dir {
// Reached filesystem root without finding go.mod; run tests from cwd.
break
}
dir = parent
}
_ = os.Chdir(dir)
os.Exit(m.Run())
}
// mockGiteaAPI starts an httptest server that simulates the Gitea API
// endpoints needed by the handlers. Returns the server and a cleanup func.
func mockGiteaAPI(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
// GET /api/v1/user/orgs — list user orgs.
mux.HandleFunc("GET /api/v1/user/orgs", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{
{"username": "test-org", "full_name": "Test Org", "avatar_url": ""},
})
})
// GET /api/v1/orgs/{org}/repos — list repos for an org.
mux.HandleFunc("GET /api/v1/orgs/{org}/repos", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{
{"id": 1, "name": "repo1", "full_name": "test-org/repo1", "owner": map[string]string{"login": "test-org"}},
{"id": 2, "name": "repo2", "full_name": "test-org/repo2", "owner": map[string]string{"login": "test-org"}},
})
})
// GET /api/v1/repos/{owner}/{repo}/issues — list issues.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/issues", func(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
if state == "" {
state = "open"
}
json.NewEncoder(w).Encode([]map[string]interface{}{
{
"id": 1, "number": 1, "title": "Test issue",
"state": state, "body": "Issue body",
"labels": []map[string]interface{}{{"id": 1, "name": "bug", "color": "d73a4a"}},
"html_url": "http://gitea.example.com/test-org/repo1/issues/1",
},
})
})
// GET /api/v1/repos/{owner}/{repo}/issues/{index} — get single issue.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/issues/{index}", func(w http.ResponseWriter, r *http.Request) {
// Check if this is a comments request.
if strings.HasSuffix(r.URL.Path, "/comments") {
json.NewEncoder(w).Encode([]map[string]interface{}{
{"id": 1, "body": "Test comment", "user": map[string]string{"login": "user1"}, "created_at": "2026-01-01T00:00:00Z"},
})
return
}
index := r.PathValue("index")
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 1, "number": mustAtoi(index), "title": "Test issue #" + index,
"state": "open", "body": "Issue body for #" + index,
"labels": []map[string]interface{}{{"id": 1, "name": "bug", "color": "d73a4a"}},
"html_url": fmt.Sprintf("http://gitea.example.com/test-org/repo1/issues/%s", index),
"assignees": []interface{}{},
})
})
// GET /api/v1/repos/{owner}/{repo}/issues/{index}/comments.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/issues/{index}/comments", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{
{"id": 1, "body": "Test comment", "user": map[string]string{"login": "user1"}, "created_at": "2026-01-01T00:00:00Z"},
})
})
// POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments — add comment.
mux.HandleFunc("POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 99, "body": "New comment", "user": map[string]string{"login": "testuser"}, "created_at": "2026-03-28T00:00:00Z",
})
})
// GET /api/v1/repos/{owner}/{repo}/labels — get repo labels.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/labels", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{
{"id": 1, "name": "bug", "color": "d73a4a"},
{"id": 2, "name": "enhancement", "color": "a2eeef"},
})
})
// GET /api/v1/repos/{owner}/{repo}/collaborators — list collaborators.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/collaborators", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{
{"login": "user1"},
{"login": "user2"},
})
})
// GET /api/v1/repos/{owner}/{repo}/pulls — list PRs.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/pulls", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{
{
"id": 1, "number": 10, "title": "Test PR",
"state": "open", "body": "PR body",
"html_url": "http://gitea.example.com/test-org/repo1/pulls/10",
"head": map[string]string{"label": "feature", "ref": "feature"},
"base": map[string]string{"label": "master", "ref": "master"},
},
})
})
// GET /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews — get PR reviews.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{})
})
// GET /api/v1/repos/{owner}/{repo}/pulls/{index} — get single PR.
mux.HandleFunc("GET /api/v1/repos/{owner}/{repo}/pulls/{index}", func(w http.ResponseWriter, r *http.Request) {
index := r.PathValue("index")
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 1, "number": mustAtoi(index), "title": "Test PR #" + index,
"state": "open", "body": "PR body for #" + index,
"html_url": fmt.Sprintf("http://gitea.example.com/test-org/repo1/pulls/%s", index),
"head": map[string]string{"label": "feature-branch", "ref": "feature-branch"},
"base": map[string]string{"label": "master", "ref": "master"},
})
})
// PATCH /api/v1/repos/{owner}/{repo}/issues/{index} — update issue state.
mux.HandleFunc("PATCH /api/v1/repos/{owner}/{repo}/issues/{index}", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{"id": 1, "state": "closed"})
})
// POST /api/v1/repos/{owner}/{repo}/issues/{index}/labels — apply labels.
mux.HandleFunc("POST /api/v1/repos/{owner}/{repo}/issues/{index}/labels", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]interface{}{{"id": 1, "name": "bug"}})
})
// POST /api/v1/repos/{owner}/{repo}/issues/{index}/assignees — assign issue.
mux.HandleFunc("POST /api/v1/repos/{owner}/{repo}/issues/{index}/assignees", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{"id": 1, "assignees": []map[string]string{{"login": "user1"}}})
})
// POST /api/v1/repos/{owner}/{repo}/issues — create issue.
mux.HandleFunc("POST /api/v1/repos/{owner}/{repo}/issues", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 42, "number": 42, "title": "New test issue", "state": "open",
})
})
// POST /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews — submit review.
mux.HandleFunc("POST /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{"id": 1, "state": "APPROVED"})
})
// POST /api/v1/markdown — render markdown.
mux.HandleFunc("POST /api/v1/markdown", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, "<p>Rendered markdown</p>")
})
return httptest.NewServer(mux)
}
// newTestHandlerWithMock creates a Handler backed by a mock Gitea API.
func newTestHandlerWithMock(t *testing.T) (*Handler, *httptest.Server) {
t.Helper()
srv := mockGiteaAPI(t)
cfg := &config.Config{
GiteaURL: srv.URL,
SessionSecret: "test-secret-that-is-at-least-32-chars-long",
ListenAddr: ":8080",
}
client := giteaclient.NewClient(cfg.GiteaURL)
h := NewHandler(cfg, client)
return h, srv
}
// reqWithToken creates an HTTP request with a token in the context.
func reqWithToken(method, path string, body string) *http.Request {
var r *http.Request
if body != "" {
r = httptest.NewRequest(method, path, strings.NewReader(body))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else {
r = httptest.NewRequest(method, path, nil)
}
ctx := context.WithValue(r.Context(), middleware.TokenContextKey, "test-token")
return r.WithContext(ctx)
}
func mustAtoi(s string) int {
n := 0
for _, c := range s {
if c >= '0' && c <= '9' {
n = n*10 + int(c-'0')
}
}
return n
}
// --- Issue #140: Integration tests for GET / dashboard handler ---
func TestIntegration_Dashboard_WithToken(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/", "")
w := httptest.NewRecorder()
h.Dashboard(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Dashboard") || !contains(body, "Gitea Mobile") {
t.Error("expected dashboard page content with full page wrapper")
}
}
func TestIntegration_Dashboard_WithOrgFilter(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/?org=test-org", "")
w := httptest.NewRecorder()
h.Dashboard(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
// --- Issue #139: Integration tests for GET /issues and GET /pulls ---
func TestIntegration_ListIssues_WithToken(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues", "")
w := httptest.NewRecorder()
h.ListIssues(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Issues") {
t.Error("expected issues page content")
}
}
func TestIntegration_ListIssues_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues", "")
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
h.ListIssues(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if contains(body, "<!DOCTYPE") {
t.Error("HTMX response should not contain DOCTYPE")
}
}
func TestIntegration_ListPulls_WithToken(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/pulls", "")
w := httptest.NewRecorder()
h.ListPulls(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Pull Requests") || !contains(body, "PRs") {
t.Error("expected pulls page content")
}
}
// --- Issue #136: Integration tests for org/repo/label/state filter query params ---
func TestIntegration_ListIssues_OrgFilter(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues?org=test-org", "")
w := httptest.NewRecorder()
h.ListIssues(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestIntegration_ListIssues_StateFilter(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues?state=closed", "")
w := httptest.NewRecorder()
h.ListIssues(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestIntegration_ListIssues_LabelFilter(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues?label=bug", "")
w := httptest.NewRecorder()
h.ListIssues(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestIntegration_ListIssues_RepoFilter(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues?org=test-org&repo=repo1", "")
w := httptest.NewRecorder()
h.ListIssues(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestIntegration_ListPulls_StateFilter(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/pulls?state=closed", "")
w := httptest.NewRecorder()
h.ListPulls(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestIntegration_ListPulls_OrgFilter(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/pulls?org=test-org", "")
w := httptest.NewRecorder()
h.ListPulls(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
// --- Issue #133: Integration tests for GET /issues/{owner}/{repo}/{index} ---
func TestIntegration_IssueDetail_ValidIndex(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("GET /issues/{owner}/{repo}/{index}", h.IssueDetail)
req := reqWithToken(http.MethodGet, "/issues/test-org/repo1/1", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Issue #1") {
t.Errorf("expected page title containing 'Issue #1', got: %s", body[:min(200, len(body))])
}
}
func TestIntegration_IssueDetail_InvalidIndex(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("GET /issues/{owner}/{repo}/{index}", h.IssueDetail)
req := reqWithToken(http.MethodGet, "/issues/test-org/repo1/abc", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
// --- Issue #134: Integration tests for GET /pulls/{owner}/{repo}/{index} ---
func TestIntegration_PullDetail_ValidIndex(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("GET /pulls/{owner}/{repo}/{index}", h.PullDetail)
req := reqWithToken(http.MethodGet, "/pulls/test-org/repo1/5", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "PR #5") {
t.Errorf("expected page title containing 'PR #5', got: %s", body[:min(200, len(body))])
}
}
func TestIntegration_PullDetail_InvalidIndex(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("GET /pulls/{owner}/{repo}/{index}", h.PullDetail)
req := reqWithToken(http.MethodGet, "/pulls/test-org/repo1/xyz", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
// --- Issue #138: Integration tests for GET /issues/new and GET /issues/new/labels ---
func TestIntegration_NewIssue_WithToken(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues/new", "")
w := httptest.NewRecorder()
h.NewIssue(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "New Issue") {
t.Error("expected 'New Issue' in response body")
}
}
func TestIntegration_NewIssueLabels_WithRepo(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues/new/labels?owner=test-org&repo=repo1", "")
w := httptest.NewRecorder()
h.NewIssueLabels(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "bug") {
t.Error("expected labels to contain 'bug'")
}
if !contains(body, "enhancement") {
t.Error("expected labels to contain 'enhancement'")
}
}
func TestIntegration_NewIssueLabels_NoRepo(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
req := reqWithToken(http.MethodGet, "/issues/new/labels", "")
w := httptest.NewRecorder()
h.NewIssueLabels(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Select a repository first") {
t.Error("expected prompt to select repository")
}
}
// --- Issue #137: Integration tests for POST /issues/{owner}/{repo}/{index}/state
// and POST /pulls/{owner}/{repo}/{index}/state ---
func TestIntegration_SetIssueState_Close(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/state", h.SetIssueState)
form := url.Values{"state": {"closed"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/state", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
loc := w.Header().Get("Location")
if loc != "/issues/test-org/repo1/1" {
t.Errorf("redirect location = %q, want /issues/test-org/repo1/1", loc)
}
}
func TestIntegration_SetIssueState_Close_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/state", h.SetIssueState)
form := url.Values{"state": {"closed"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/state", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "closed") {
t.Error("expected HTMX response to contain 'closed' state")
}
if !contains(body, "Reopen") {
t.Error("expected HTMX response to contain 'Reopen' button")
}
}
func TestIntegration_SetIssueState_Reopen(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/state", h.SetIssueState)
form := url.Values{"state": {"open"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/state", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "open") {
t.Error("expected HTMX response to contain 'open' state")
}
}
func TestIntegration_SetPullState_Close(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/state", h.SetPullState)
form := url.Values{"state": {"closed"}}
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/state", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_SetPullState_Close_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/state", h.SetPullState)
form := url.Values{"state": {"closed"}}
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/state", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "closed") {
t.Error("expected HTMX response to contain 'closed' state")
}
if !contains(body, "Reopen PR") {
t.Error("expected HTMX response to contain 'Reopen PR' button")
}
}
// --- Issue #135: Integration tests for POST /issues/{owner}/{repo}/{index}/assignees ---
func TestIntegration_AssignIssue_Valid(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/assignees", h.AssignIssue)
form := url.Values{"assignee": {"user1"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/assignees", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_AssignIssue_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/assignees", h.AssignIssue)
form := url.Values{"assignee": {"user1"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/assignees", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Assigned to user1") {
t.Errorf("expected 'Assigned to user1' in HTMX response, got: %s", body)
}
}
func TestIntegration_AssignIssue_MissingAssignee(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/assignees", h.AssignIssue)
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/assignees", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
// --- Issue #118: Integration tests for CloseIssue and AddComment ---
func TestIntegration_CloseIssue(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/close", h.CloseIssue)
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/close", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_CloseIssue_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/close", h.CloseIssue)
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/close", "")
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
if w.Header().Get("HX-Redirect") == "" {
t.Error("expected HX-Redirect header for HTMX close")
}
}
func TestIntegration_AddComment_Valid(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/comments", h.AddComment)
form := url.Values{"body": {"This is a test comment"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/comments", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_AddComment_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/comments", h.AddComment)
form := url.Values{"body": {"HTMX comment"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/comments", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "comment") {
t.Error("expected comment card in HTMX response")
}
}
// --- Issue #113: Integration tests for POST /issues create-issue handler ---
func TestIntegration_CreateIssue_Valid(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
form := url.Values{
"owner": {"test-org"},
"repo": {"repo1"},
"title": {"Test issue title"},
"body": {"Test issue body"},
}
req := reqWithToken(http.MethodPost, "/issues", form.Encode())
w := httptest.NewRecorder()
h.CreateIssue(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
loc := w.Header().Get("Location")
if !contains(loc, "/issues/test-org/repo1/42") {
t.Errorf("redirect location = %q, want to contain /issues/test-org/repo1/42", loc)
}
}
func TestIntegration_CreateIssue_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
form := url.Values{
"owner": {"test-org"},
"repo": {"repo1"},
"title": {"HTMX issue"},
}
req := reqWithToken(http.MethodPost, "/issues", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
h.CreateIssue(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
if w.Header().Get("HX-Redirect") == "" {
t.Error("expected HX-Redirect header for HTMX create")
}
}
// --- Issue #111: Integration tests for POST /pulls/{owner}/{repo}/{index}/review ---
func TestIntegration_SubmitReview_Approve(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/review", h.SubmitReview)
form := url.Values{"event": {"APPROVED"}, "body": {"LGTM"}}
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/review", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_SubmitReview_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/review", h.SubmitReview)
form := url.Values{"event": {"APPROVED"}, "body": {"Looks good"}}
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/review", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Review submitted") {
t.Error("expected 'Review submitted' in HTMX response")
}
}
func TestIntegration_SubmitReview_RequestChanges(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/review", h.SubmitReview)
form := url.Values{"event": {"REQUEST_CHANGES"}, "body": {"Please fix the error handling"}}
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/review", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
// --- Issue #110: Integration tests for POST /issues/{owner}/{repo}/{index}/labels ---
func TestIntegration_ApplyLabels_Valid(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
form := url.Values{"label_id": {"1", "2"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/labels", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_ApplyLabels_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
form := url.Values{"label_id": {"1"}}
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/labels", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Labels applied") {
t.Error("expected 'Labels applied' in HTMX response")
}
}
func TestIntegration_ApplyLabels_NoLabels(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /issues/{owner}/{repo}/{index}/labels", h.ApplyLabels)
req := reqWithToken(http.MethodPost, "/issues/test-org/repo1/1/labels", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
// --- Issue #124: Integration tests for GET /settings and POST /settings ---
func newSettingsHandler() *SettingsHandler {
return &SettingsHandler{
SessionSecret: "test-secret-that-is-at-least-32-chars-long",
SecureCookies: false, // Disable for test (no TLS).
}
}
func TestIntegration_Settings_Get(t *testing.T) {
sh := newSettingsHandler()
req := httptest.NewRequest(http.MethodGet, "/settings", nil)
w := httptest.NewRecorder()
sh.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestIntegration_Settings_PostSave_EmptyToken(t *testing.T) {
sh := newSettingsHandler()
form := url.Values{"action": {"save"}, "token": {""}}
req := httptest.NewRequest(http.MethodPost, "/settings", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
sh.ServeHTTP(w, req)
// Empty token should show an error, not redirect.
if w.Code == http.StatusSeeOther {
t.Error("expected error response for empty token, not redirect")
}
}
func TestIntegration_Settings_PostSave_ValidToken(t *testing.T) {
sh := newSettingsHandler()
form := url.Values{"action": {"save"}, "token": {"my-gitea-token"}}
req := httptest.NewRequest(http.MethodPost, "/settings", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
sh.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d (redirect after saving token)", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_Settings_PostLogout(t *testing.T) {
sh := newSettingsHandler()
form := url.Values{"action": {"logout"}}
req := httptest.NewRequest(http.MethodPost, "/settings", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
sh.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
}
func TestIntegration_Settings_MethodNotAllowed(t *testing.T) {
sh := newSettingsHandler()
req := httptest.NewRequest(http.MethodDelete, "/settings", nil)
w := httptest.NewRecorder()
sh.ServeHTTP(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}