go-scm/gitea/testhelper_test.go
Claude 9db37c6fb3
test: add comprehensive unit tests for forge/, gitea/, git/, agentci/
Phase 1 test coverage for the three 0% packages plus agentci/ improvement:

- git/ (0% -> 79.5%): RepoStatus methods, status parsing with real temp
  repos, multi-repo parallel status, Push/Pull error paths, ahead/behind
  with bare remote, context cancellation, GitError, IsNonFastForward,
  service DirtyRepos/AheadRepos filtering

- forge/ (0% -> 91.2%): All SDK wrapper functions tested via httptest mock
  server — client creation, repos, issues, PRs, labels, webhooks, orgs,
  meta, config resolution, SetPRDraft raw HTTP endpoint

- gitea/ (0% -> 89.2%): All SDK wrapper functions tested via httptest mock
  server — client creation, repos, issues, PRs, meta, config resolution

- agentci/ (56% -> 94.5%): Clotho DeterminePlan all code paths, security
  helpers (SanitizePath, EscapeShellArg, SecureSSHCommand, MaskToken)

Key findings documented in FINDINGS.md:
- Forgejo SDK validates token via HTTP on NewClient()
- SDK route patterns differ from public API docs (/org/ vs /orgs/)
- Gitea SDK requires auth token for GitHub mirror creation
- Config resolution priority verified: config file < env vars < flags

Co-Authored-By: Charon <developers@lethean.io>
2026-02-20 00:59:46 +00:00

192 lines
6.2 KiB
Go

package gitea
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// newMockGiteaServer creates an httptest.Server that mimics the Gitea API
// endpoints used during client initialisation and common operations.
func newMockGiteaServer(t *testing.T) *httptest.Server {
t.Helper()
mux := newGiteaMux()
return httptest.NewServer(mux)
}
// newGiteaMux creates an http.ServeMux with standard Gitea API responses.
func newGiteaMux() *http.ServeMux {
mux := http.NewServeMux()
// The Gitea SDK calls /api/v1/version during NewClient().
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, map[string]string{"version": "1.21.0"})
})
// User repos listing.
mux.HandleFunc("/api/v1/user/repos", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, []map[string]any{
{"id": 1, "name": "repo-a", "full_name": "test-user/repo-a", "owner": map[string]any{"login": "test-user"}},
{"id": 2, "name": "repo-b", "full_name": "test-user/repo-b", "owner": map[string]any{"login": "test-user"}},
})
})
// Org repos listing.
mux.HandleFunc("/api/v1/orgs/test-org/repos", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, []map[string]any{
{"id": 10, "name": "org-repo", "full_name": "test-org/org-repo", "owner": map[string]any{"login": "test-org", "id": 100}},
})
})
// Create org repo (Gitea SDK uses /org/ not /orgs/).
mux.HandleFunc("/api/v1/org/test-org/repos", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
w.WriteHeader(http.StatusCreated)
jsonResponse(w, map[string]any{
"id": 20, "name": "new-repo", "full_name": "test-org/new-repo",
"owner": map[string]any{"login": "test-org"},
})
return
}
jsonResponse(w, []map[string]any{})
})
// Get/delete single repo.
mux.HandleFunc("/api/v1/repos/test-org/org-repo", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodDelete {
w.WriteHeader(http.StatusNoContent)
return
}
jsonResponse(w, map[string]any{
"id": 10, "name": "org-repo", "full_name": "test-org/org-repo",
"owner": map[string]any{"login": "test-org"},
})
})
// Issues.
mux.HandleFunc("/api/v1/repos/test-org/org-repo/issues", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
w.WriteHeader(http.StatusCreated)
jsonResponse(w, map[string]any{
"id": 1, "number": 1, "title": "Test Issue", "state": "open",
"body": "Issue body text",
})
return
}
jsonResponse(w, []map[string]any{
{"id": 1, "number": 1, "title": "Issue 1", "state": "open", "body": "First issue"},
{"id": 2, "number": 2, "title": "Issue 2", "state": "closed", "body": "Second issue"},
})
})
// Single issue.
mux.HandleFunc("/api/v1/repos/test-org/org-repo/issues/1", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, map[string]any{
"id": 1, "number": 1, "title": "Issue 1", "state": "open",
"body": "First issue body",
})
})
// Issue comments.
mux.HandleFunc("/api/v1/repos/test-org/org-repo/issues/1/comments", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, []map[string]any{
{"id": 100, "body": "comment 1", "user": map[string]any{"login": "user1"}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z"},
{"id": 101, "body": "comment 2", "user": map[string]any{"login": "user2"}, "created_at": "2026-01-02T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z"},
})
})
// Pull requests.
mux.HandleFunc("/api/v1/repos/test-org/org-repo/pulls", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, []map[string]any{
{
"id": 1, "number": 1, "title": "PR 1", "state": "open",
"head": map[string]any{"ref": "feature", "label": "feature"},
"base": map[string]any{"ref": "main", "label": "main"},
},
})
})
// Single pull request.
mux.HandleFunc("/api/v1/repos/test-org/org-repo/pulls/1", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, map[string]any{
"id": 1, "number": 1, "title": "PR 1", "state": "open",
"merged": false,
"head": map[string]any{"ref": "feature", "label": "feature"},
"base": map[string]any{"ref": "main", "label": "main"},
"user": map[string]any{"login": "author"},
"labels": []map[string]any{{"name": "enhancement"}},
"assignees": []map[string]any{
{"login": "dev1"},
},
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-16T12:00:00Z",
})
})
// Migrate repo.
mux.HandleFunc("/api/v1/repos/migrate", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
jsonResponse(w, map[string]any{
"id": 40, "name": "mirrored-repo", "full_name": "test-org/mirrored-repo",
"owner": map[string]any{"login": "test-org"},
"mirror": true,
})
})
// Fallback for PATCH requests and unmatched routes.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPatch && strings.Contains(r.URL.Path, "/pulls/") {
jsonResponse(w, map[string]any{
"number": 1, "title": "test PR", "state": "open",
})
return
}
http.NotFound(w, r)
})
return mux
}
// jsonResponse writes a JSON response.
func jsonResponse(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(data)
}
// newTestClient creates a Client backed by the mock server.
func newTestClient(t *testing.T) (*Client, *httptest.Server) {
t.Helper()
srv := newMockGiteaServer(t)
client, err := New(srv.URL, "test-token")
if err != nil {
srv.Close()
t.Fatalf("failed to create test client: %v", err)
}
return client, srv
}
// newErrorServer creates a mock server that returns errors for all API calls.
func newErrorServer(t *testing.T) (*Client, *httptest.Server) {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, map[string]string{"version": "1.21.0"})
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
})
srv := httptest.NewServer(mux)
client, err := New(srv.URL, "token")
if err != nil {
srv.Close()
t.Fatalf("failed to create error server client: %v", err)
}
return client, srv
}