gitlink-cli/shortcuts/workflow/triage_fetch_test.go

261 lines
8.4 KiB
Go

package workflow
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestFetchIssuesForTriageNormalizesAPIResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if got := r.URL.Query().Get("category"); got != "opened" {
t.Fatalf("category query = %q, want opened", got)
}
if got := r.URL.Query().Get("state"); got != "" {
t.Fatalf("state query = %q, want empty", got)
}
if got := r.URL.Query().Get("limit"); got != "30" {
t.Fatalf("limit query = %q, want 30", got)
}
writeWorkflowJSON(t, w, map[string]interface{}{
"issues": []map[string]interface{}{
{
"id": 12345,
"project_issues_index": 7,
"subject": "Crash on login",
"description": "panic error when running login",
"status": "open",
"author": map[string]interface{}{"login": "alice"},
"labels": []map[string]interface{}{{"name": "bug"}, {"name": "login"}},
"created_at": "2026-05-01T10:00:00Z",
"updated_at": "2026-05-02T10:00:00Z",
"comments_count": 2,
"html_url": "https://www.gitlink.org.cn/owner/repo/issues/7",
},
},
})
}))
defer server.Close()
ctx := workflowTestContext(server)
issues, err := FetchIssuesForTriage(ctx, TriageFetchOptions{State: "open", Limit: 30, Page: 1})
if err != nil {
t.Fatalf("FetchIssuesForTriage returned error: %v", err)
}
if len(issues) != 1 {
t.Fatalf("len(issues) = %d, want 1", len(issues))
}
issue := issues[0]
if issue.ID != "12345" {
t.Fatalf("ID = %q, want 12345", issue.ID)
}
if issue.Number != 7 {
t.Fatalf("Number = %d, want 7", issue.Number)
}
if issue.Title != "Crash on login" {
t.Fatalf("Title = %q, want Crash on login", issue.Title)
}
if issue.Author != "alice" {
t.Fatalf("Author = %q, want alice", issue.Author)
}
if len(issue.Labels) != 2 || issue.Labels[0] != "bug" || issue.Labels[1] != "login" {
t.Fatalf("Labels = %v, want [bug login]", issue.Labels)
}
if issue.CreatedAt.IsZero() || issue.UpdatedAt.IsZero() {
t.Fatalf("expected parsed timestamps, got created=%v updated=%v", issue.CreatedAt, issue.UpdatedAt)
}
}
func TestFetchIssuesForTriageSupportsDataString(t *testing.T) {
payload, err := json.Marshal([]map[string]interface{}{
{
"number": 3,
"title": "README typo",
"body": "documentation example typo",
"state": "open",
},
})
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
writeWorkflowJSON(t, w, map[string]interface{}{"data": string(payload)})
}))
defer server.Close()
issues, err := FetchIssuesForTriage(workflowTestContext(server), TriageFetchOptions{Limit: 10})
if err != nil {
t.Fatalf("FetchIssuesForTriage returned error: %v", err)
}
if len(issues) != 1 {
t.Fatalf("len(issues) = %d, want 1", len(issues))
}
if issues[0].Number != 3 || issues[0].Title != "README typo" {
t.Fatalf("issue = %+v, want number 3 title README typo", issues[0])
}
}
func TestFetchIssuesForTriageEmptyResponseReturnsError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeWorkflowJSON(t, w, map[string]interface{}{"issues": []map[string]interface{}{}})
}))
defer server.Close()
_, err := FetchIssuesForTriage(workflowTestContext(server), TriageFetchOptions{Limit: 10})
if err == nil {
t.Fatal("FetchIssuesForTriage returned nil error for empty response")
}
if !strings.Contains(err.Error(), "no issues found") {
t.Fatalf("error = %v, want empty-response message", err)
}
}
func TestFetchIssuesForTriageNormalizesLabelAndAuthorShapes(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.URL.Query().Get("page"); got != "1" {
t.Fatalf("page query = %q, want 1", got)
}
if got := r.URL.Query().Get("limit"); got != "3" {
t.Fatalf("limit query = %q, want 3", got)
}
writeWorkflowJSON(t, w, map[string]interface{}{
"issues": []map[string]interface{}{
{
"number": 1,
"body": "panic on install",
"labels": []string{"bug", "help wanted"},
"author": "alice",
},
{
"number": 2,
"title": "token leak",
"labels": []map[string]interface{}{{"name": "bug"}, {"name": "security"}},
"user": map[string]interface{}{"login": "bob"},
},
{
"number": 3,
"title": "README typo",
"labels": []map[string]interface{}{{"title": "docs"}},
"creator": map[string]interface{}{"name": "carol"},
},
},
})
}))
defer server.Close()
issues, err := FetchIssuesForTriage(workflowTestContext(server), TriageFetchOptions{State: "open", Limit: 3, Page: 1})
if err != nil {
t.Fatalf("FetchIssuesForTriage returned error: %v", err)
}
if len(issues) != 3 {
t.Fatalf("len(issues) = %d, want 3", len(issues))
}
if issues[0].Title != "" || issues[0].Body != "panic on install" {
t.Fatalf("issue[0] = %+v, want body-only item with empty title", issues[0])
}
if got := strings.Join(issues[0].Labels, ","); got != "bug,help wanted" {
t.Fatalf("issue[0].Labels = %q, want bug,help wanted", got)
}
if issues[0].Author != "alice" {
t.Fatalf("issue[0].Author = %q, want alice", issues[0].Author)
}
if got := strings.Join(issues[1].Labels, ","); got != "bug,security" {
t.Fatalf("issue[1].Labels = %q, want bug,security", got)
}
if issues[1].Author != "bob" {
t.Fatalf("issue[1].Author = %q, want bob", issues[1].Author)
}
if got := strings.Join(issues[2].Labels, ","); got != "docs" {
t.Fatalf("issue[2].Labels = %q, want docs", got)
}
if issues[2].Author != "carol" {
t.Fatalf("issue[2].Author = %q, want carol", issues[2].Author)
}
}
func TestFetchIssuesForTriageRespectsLimitAndReportsRequestErrors(t *testing.T) {
requestCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++
if got := r.URL.Query().Get("page"); got != "2" {
t.Fatalf("page query = %q, want 2", got)
}
if got := r.URL.Query().Get("limit"); got != "1" {
t.Fatalf("limit query = %q, want 1", got)
}
writeWorkflowJSON(t, w, map[string]interface{}{
"issues": []map[string]interface{}{
{"number": 1, "title": "first"},
{"number": 2, "title": "second"},
},
})
}))
defer server.Close()
issues, err := FetchIssuesForTriage(workflowTestContext(server), TriageFetchOptions{State: "open", Limit: 1, Page: 2})
if err != nil {
t.Fatalf("FetchIssuesForTriage returned error: %v", err)
}
if requestCount != 1 {
t.Fatalf("requestCount = %d, want 1", requestCount)
}
if len(issues) != 1 {
t.Fatalf("len(issues) = %d, want 1", len(issues))
}
if issues[0].Number != 1 {
t.Fatalf("issues[0].Number = %d, want 1", issues[0].Number)
}
}
func TestFetchIssuesForTriageReportsGitLinkErrorInBody(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
writeWorkflowJSON(t, w, map[string]interface{}{
"status": 403,
"message": "permission denied",
})
}))
defer server.Close()
_, err := FetchIssuesForTriage(workflowTestContext(server), TriageFetchOptions{Limit: 10})
if err == nil {
t.Fatal("FetchIssuesForTriage returned nil error for error-in-body response")
}
if !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("error = %v, want permission denied", err)
}
}
func workflowTestContext(server *httptest.Server) *common.RuntimeContext {
return &common.RuntimeContext{
Client: &client.Client{
HTTP: server.Client(),
BaseURL: server.URL,
},
Owner: "owner",
Repo: "repo",
Format: "json",
Args: map[string]string{},
}
}
func writeWorkflowJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(payload); err != nil {
t.Fatalf("failed to write response: %v", err)
}
}