package gitea import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) func TestNewClient(t *testing.T) { c := NewClient("https://gitea.example.com") if c.baseURL != "https://gitea.example.com" { t.Errorf("baseURL = %q, want %q", c.baseURL, "https://gitea.example.com") } if c.maxConcurrent != 5 { t.Errorf("maxConcurrent = %d, want 5", c.maxConcurrent) } if c.cacheTTL != 30*time.Second { t.Errorf("cacheTTL = %v, want 30s", c.cacheTTL) } } func TestNewClient_TrailingSlash(t *testing.T) { c := NewClient("https://gitea.example.com/") if c.baseURL != "https://gitea.example.com" { t.Errorf("baseURL = %q, want trailing slash removed", c.baseURL) } } func TestCache(t *testing.T) { c := NewClient("https://gitea.example.com") // Cache miss. _, ok := c.getFromCache("key1") if ok { t.Error("expected cache miss") } // Cache set and hit. c.setCache("key1", "value1") val, ok := c.getFromCache("key1") if !ok { t.Fatal("expected cache hit") } if val.(string) != "value1" { t.Errorf("got %q, want %q", val, "value1") } // Invalidate. c.invalidateCache("key") _, ok = c.getFromCache("key1") if ok { t.Error("expected cache miss after invalidation") } } func TestCacheExpiry(t *testing.T) { c := NewClient("https://gitea.example.com") c.cacheTTL = 1 * time.Millisecond c.setCache("key1", "value1") time.Sleep(5 * time.Millisecond) _, ok := c.getFromCache("key1") if ok { t.Error("expected cache miss after TTL expiry") } } func TestInvalidateAll(t *testing.T) { c := NewClient("https://gitea.example.com") c.setCache("key1", "value1") c.setCache("key2", "value2") c.InvalidateAll() _, ok1 := c.getFromCache("key1") _, ok2 := c.getFromCache("key2") if ok1 || ok2 { t.Error("expected all cache entries to be invalidated") } } func TestPriorityScore(t *testing.T) { tests := []struct { labels []string want int }{ {[]string{"P1", "bug"}, 1}, {[]string{"P2"}, 2}, {[]string{"P3", "enhancement"}, 3}, {[]string{"bug", "enhancement"}, 4}, {nil, 4}, } for _, tt := range tests { got := priorityScore(tt.labels) if got != tt.want { t.Errorf("priorityScore(%v) = %d, want %d", tt.labels, got, tt.want) } } } func TestListOrgs(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/user/orgs" { t.Errorf("unexpected path: %s", r.URL.Path) http.NotFound(w, r) return } if r.Header.Get("Authorization") != "token test-token" { t.Error("missing or wrong Authorization header") } orgs := []Org{ {Name: "org1", FullName: "Organization 1"}, {Name: "org2", FullName: "Organization 2"}, } json.NewEncoder(w).Encode(orgs) })) defer server.Close() c := NewClient(server.URL) orgs, err := c.ListOrgs(context.Background(), "test-token") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(orgs) != 2 { t.Fatalf("got %d orgs, want 2", len(orgs)) } if orgs[0].Name != "org1" { t.Errorf("orgs[0].Name = %q, want %q", orgs[0].Name, "org1") } } func TestListOrgs_Cached(t *testing.T) { callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ json.NewEncoder(w).Encode([]Org{{Name: "org1"}}) })) defer server.Close() c := NewClient(server.URL) // First call should hit the server. _, err := c.ListOrgs(context.Background(), "test-token") if err != nil { t.Fatalf("unexpected error: %v", err) } // Second call should use cache. _, err = c.ListOrgs(context.Background(), "test-token") if err != nil { t.Fatalf("unexpected error: %v", err) } if callCount != 1 { t.Errorf("server called %d times, want 1 (cached)", callCount) } } func TestListOrgRepos(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { repos := []Repo{ {ID: 1, Name: "repo1", FullName: "org1/repo1"}, {ID: 2, Name: "repo2", FullName: "org1/repo2"}, } json.NewEncoder(w).Encode(repos) })) defer server.Close() c := NewClient(server.URL) repos, err := c.ListOrgRepos(context.Background(), "test-token", "org1") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(repos) != 2 { t.Fatalf("got %d repos, want 2", len(repos)) } } func TestGetTriageQueue_Sorting(t *testing.T) { queue := []TriageItem{ {Title: "low", Labels: []string{"P3"}, UpdatedAt: time.Now()}, {Title: "high", Labels: []string{"P1"}, UpdatedAt: time.Now()}, {Title: "medium", Labels: []string{"P2"}, UpdatedAt: time.Now()}, {Title: "none", Labels: nil, UpdatedAt: time.Now()}, } // Apply the same sort as GetTriageQueue. sortTriageQueue(queue) expected := []string{"high", "medium", "low", "none"} for i, item := range queue { if item.Title != expected[i] { t.Errorf("queue[%d].Title = %q, want %q", i, item.Title, expected[i]) } } } func TestCloseIssue(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPatch { t.Errorf("expected PATCH, got %s", r.Method) } if r.URL.Path != "/api/v1/repos/owner1/repo1/issues/42" { t.Errorf("unexpected path: %s", r.URL.Path) } if r.Header.Get("Authorization") != "token test-token" { t.Error("missing or wrong Authorization header") } var body map[string]string if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatalf("failed to decode body: %v", err) } if body["state"] != "closed" { t.Errorf("expected state=closed, got %q", body["state"]) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"state": "closed"}) })) defer server.Close() c := NewClient(server.URL) c.setCache("issues-org1", "should-be-invalidated") err := c.CloseIssue(context.Background(), "test-token", "owner1", "repo1", 42) if err != nil { t.Fatalf("unexpected error: %v", err) } // Verify cache was invalidated. _, ok := c.getFromCache("issues-org1") if ok { t.Error("expected cache to be invalidated after CloseIssue") } } func TestPostComment(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } if r.URL.Path != "/api/v1/repos/owner1/repo1/issues/42/comments" { t.Errorf("unexpected path: %s", r.URL.Path) } var body map[string]string if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatalf("failed to decode body: %v", err) } if body["body"] != "test comment" { t.Errorf("expected body='test comment', got %q", body["body"]) } comment := map[string]interface{}{ "id": 1, "body": body["body"], "user": map[string]string{"login": "testuser"}, "created_at": "2026-03-26T12:00:00Z", } json.NewEncoder(w).Encode(comment) })) defer server.Close() c := NewClient(server.URL) c.setCache("issues-org1", "should-be-invalidated") comment, err := c.PostComment(context.Background(), "test-token", "owner1", "repo1", 42, "test comment") if err != nil { t.Fatalf("unexpected error: %v", err) } if comment.Body != "test comment" { t.Errorf("comment.Body = %q, want %q", comment.Body, "test comment") } if comment.User != "testuser" { t.Errorf("comment.User = %q, want %q", comment.User, "testuser") } if comment.ID != 1 { t.Errorf("comment.ID = %d, want 1", comment.ID) } // Verify cache was invalidated. _, ok := c.getFromCache("issues-org1") if ok { t.Error("expected cache to be invalidated after PostComment") } } // sortTriageQueue is a test helper applying the same sort as GetTriageQueue. func sortTriageQueue(queue []TriageItem) { for i := 0; i < len(queue); i++ { for j := i + 1; j < len(queue); j++ { pi := priorityScore(queue[i].Labels) pj := priorityScore(queue[j].Labels) if pj < pi || (pj == pi && queue[j].UpdatedAt.After(queue[i].UpdatedAt)) { queue[i], queue[j] = queue[j], queue[i] } } } }