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, "
Rendered markdown
") }) 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, "