Files
projax/gitea/issues_test.go
mAi 1ffbfc6e69 feat(phase 2.d gitea): read-only issue ingest on items with gitea-repo links
gitea package (new): minimal client mirroring caldav's structure
- client.go: token auth, 5s timeout, ErrNotFound
- issues.go: ListIssues(owner, repo, opts) hitting
  /repos/{o}/{r}/issues?type=issues&state=…&since=…, ParseRepoRef,
  RepoHTMLURL. PullRequest-flagged rows dropped server- and client-side.
- httptest stubs covering parse, 404, ParseRepoRef variants.

web wiring:
- Server.Gitea optional GiteaDeps (Client + in-memory 3-min TTL cache
  keyed by owner/repo|state).
- detailIssues iterates every gitea-repo link, sums open issues, captures
  last-30d closed (≤20) into a disclosure. Per-repo failures surface as
  banner; one missing repo never blanks the section.
- relativeTime renders "Nm/h/d ago" / "yesterday" / fallback date.

Templates:
- issues_section.tmpl: per-repo block, header "Issues (n) + ↗ Gitea repo",
  rows with #N · title · labels · milestone · assignees · updated.
  Titles open in new tab.
- detail.tmpl: include the partial when Gitea is on and issues != nil.
- CSS: matches the Tasks section visual language.

main.go: GITEA_URL gates the integration (off when unset). GITEA_URL set
but GITEA_TOKEN missing → refuse to start.

deploy/dokploy.yaml: GITEA_URL env + GITEA_TOKEN secret added.

docs/design.md: new §6 mirroring §5's structure (link model, listing
semantics, caching, env contract, parked items).
2026-05-15 17:27:01 +02:00

144 lines
3.7 KiB
Go

package gitea
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func newFakeGitea(t *testing.T, body string) (*Client, *httptest.Server) {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/repos/m/projax/issues", func(w http.ResponseWriter, r *http.Request) {
if got, want := r.Header.Get("Authorization"), "token tok-123"; got != want {
t.Errorf("Authorization = %q, want %q", got, want)
}
if r.URL.Query().Get("type") != "issues" {
t.Errorf("type query = %q", r.URL.Query().Get("type"))
}
w.WriteHeader(200)
_, _ = io.WriteString(w, body)
})
mux.HandleFunc("/api/v1/repos/missing/repo/issues", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "{}", http.StatusNotFound)
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
c := New(srv.URL, "tok-123")
return c, srv
}
func TestListIssuesParse(t *testing.T) {
body := `[
{
"number": 223,
"title": "Integration tests leak project rows",
"state": "open",
"html_url": "https://mgit.msbls.de/m/projax/issues/223",
"updated_at": "2026-05-15T13:02:33Z",
"labels": [
{"name": "bug"},
{"name": "done"}
],
"assignees": [
{"login": "mAi"},
{"login": "knuth"}
],
"milestone": {"title": "Phase 2"}
},
{
"number": 224,
"title": "A PR that snuck through",
"state": "open",
"html_url": "https://mgit.msbls.de/m/projax/pulls/224",
"updated_at": "2026-05-15T13:00:00Z",
"pull_request": {}
}
]`
c, _ := newFakeGitea(t, body)
got, err := c.ListIssues(context.Background(), "m", "projax", ListOpts{State: "open"})
if err != nil {
t.Fatalf("ListIssues: %v", err)
}
if len(got) != 1 {
t.Fatalf("expected 1 issue (PR filtered out), got %d (%+v)", len(got), got)
}
iss := got[0]
if iss.Number != 223 || iss.Title != "Integration tests leak project rows" {
t.Errorf("issue mismatch: %+v", iss)
}
if got, want := iss.Labels, []string{"bug", "done"}; !equalStrings(got, want) {
t.Errorf("Labels = %v, want %v", got, want)
}
if got, want := iss.Assignees, []string{"mAi", "knuth"}; !equalStrings(got, want) {
t.Errorf("Assignees = %v, want %v", got, want)
}
if iss.Milestone != "Phase 2" {
t.Errorf("Milestone = %q", iss.Milestone)
}
if iss.UpdatedAt.IsZero() {
t.Error("UpdatedAt zero")
}
}
func TestListIssuesNotFound(t *testing.T) {
c, _ := newFakeGitea(t, "[]")
_, err := c.ListIssues(context.Background(), "missing", "repo", ListOpts{})
if err != ErrNotFound {
t.Errorf("expected ErrNotFound, got %v", err)
}
}
func TestParseRepoRef(t *testing.T) {
for _, tc := range []struct {
in, owner, repo string
}{
{"m/projax", "m", "projax"},
{"HL/mWorkRepo", "HL", "mWorkRepo"},
{" m/mAi ", "m", "mAi"},
{"missing-slash", "", ""},
{"/leading", "", ""},
{"trailing/", "", ""},
{"", "", ""},
} {
o, r := ParseRepoRef(tc.in)
if o != tc.owner || r != tc.repo {
t.Errorf("ParseRepoRef(%q) = (%q,%q), want (%q,%q)", tc.in, o, r, tc.owner, tc.repo)
}
}
}
func TestRepoHTMLURL(t *testing.T) {
c := New("https://mgit.msbls.de/", "")
if got, want := c.RepoHTMLURL("m", "projax"), "https://mgit.msbls.de/m/projax"; got != want {
t.Errorf("RepoHTMLURL = %q, want %q", got, want)
}
}
// Smoke: 5s timeout should be the default.
func TestDefaultTimeout(t *testing.T) {
c := New("https://example", "")
if c.HTTPClient.Timeout != 5*time.Second {
t.Errorf("Timeout = %v, want 5s", c.HTTPClient.Timeout)
}
if !strings.HasPrefix(c.BaseURL, "https://example") {
t.Errorf("BaseURL = %q", c.BaseURL)
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}