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).
144 lines
3.7 KiB
Go
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
|
|
}
|