gitlink-cli/shortcuts/issue/issue_test.go

547 lines
16 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package issue
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestIssueClosePreservesCurrentDescription(t *testing.T) {
var updatePayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
writeJSON(t, w, map[string]interface{}{
"subject": "Existing title",
"description": "Existing description",
})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
updatePayload = decodeJSON(t, r)
writeJSON(t, w, updatePayload)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "close", map[string]string{"number": "42"})
if err != nil {
t.Fatalf("close shortcut failed: %v", err)
}
assertEqual(t, updatePayload["subject"], "Existing title")
assertEqual(t, updatePayload["description"], "Existing description")
assertEqual(t, updatePayload["status_id"], float64(5))
}
func TestIssueUpdatePreservesCurrentDescriptionWhenChangingTitleAndState(t *testing.T) {
var updatePayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
writeJSON(t, w, map[string]interface{}{
"subject": "Existing title",
"description": "Existing description",
})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
updatePayload = decodeJSON(t, r)
writeJSON(t, w, updatePayload)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "update", map[string]string{
"number": "42",
"title": "New title",
"state": "closed",
})
if err != nil {
t.Fatalf("update shortcut failed: %v", err)
}
assertEqual(t, updatePayload["subject"], "New title")
assertEqual(t, updatePayload["description"], "Existing description")
assertEqual(t, updatePayload["status_id"], float64(5))
}
func TestIssueUpdatePreservesCurrentSubjectWhenChangingDescription(t *testing.T) {
var updatePayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
writeJSON(t, w, map[string]interface{}{
"subject": "Existing title",
"description": "Existing description",
})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
updatePayload = decodeJSON(t, r)
writeJSON(t, w, updatePayload)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "update", map[string]string{
"number": "42",
"body": "New description",
})
if err != nil {
t.Fatalf("update shortcut failed: %v", err)
}
assertEqual(t, updatePayload["subject"], "Existing title")
assertEqual(t, updatePayload["description"], "New description")
}
func TestBatchClosePreservesCurrentDescription(t *testing.T) {
var updatePayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
writeJSON(t, w, map[string]interface{}{
"subject": "Existing title",
"description": "Existing description",
})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
updatePayload = decodeJSON(t, r)
writeJSON(t, w, updatePayload)
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "batch-close", map[string]string{
"numbers": "42",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-close shortcut failed: %v", err)
}
assertEqual(t, updatePayload["subject"], "Existing title")
assertEqual(t, updatePayload["description"], "Existing description")
assertEqual(t, updatePayload["status_id"], float64(5))
}
func runIssueShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
shortcut := findIssueShortcut(t, name)
ctx := &common.RuntimeContext{
Client: &client.Client{
HTTP: server.Client(),
BaseURL: server.URL,
},
Owner: "owner",
Repo: "repo",
Format: "json",
Args: args,
}
return shortcut.Run(ctx)
}
func findIssueShortcut(t *testing.T, name string) *common.Shortcut {
t.Helper()
for _, shortcut := range Shortcuts() {
if shortcut.Name == name {
return shortcut
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func newIssueTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func decodeJSON(t *testing.T, r *http.Request) map[string]interface{} {
t.Helper()
var payload map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
return payload
}
func writeJSON(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)
}
}
func assertEqual(t *testing.T, got interface{}, want interface{}) {
t.Helper()
if got != want {
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
}
}
func TestIssueList(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues.json" {
writeJSON(t, w, map[string]interface{}{
"issues": []map[string]interface{}{
{"id": float64(1), "subject": "bug", "status_id": float64(1)},
{"id": float64(2), "subject": "feature", "status_id": float64(5)},
},
"total_count": float64(2),
})
return
}
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runIssueShortcut(t, server, "list", map[string]string{})
if err != nil {
t.Fatalf("issue list failed: %v", err)
}
}
func TestIssueCreate(t *testing.T) {
var createPayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues.json" {
createPayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{
"id": float64(99), "subject": "new bug",
})
return
}
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runIssueShortcut(t, server, "create", map[string]string{
"title": "new bug",
"body": "steps to reproduce",
"assignee": "42",
})
if err != nil {
t.Fatalf("issue create failed: %v", err)
}
assertEqual(t, createPayload["subject"], "new bug")
assertEqual(t, createPayload["description"], "steps to reproduce")
assertEqual(t, createPayload["assigned_to_id"], "42")
}
func TestIssueView(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/1.json" {
writeJSON(t, w, map[string]interface{}{
"id": float64(1), "subject": "test issue", "status_id": float64(1),
})
return
}
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runIssueShortcut(t, server, "view", map[string]string{"number": "1"})
if err != nil {
t.Fatalf("issue view failed: %v", err)
}
}
func TestIssueComment(t *testing.T) {
var commentPayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/1/journals.json" {
commentPayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{
"id": float64(1), "notes": "looks good",
})
return
}
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runIssueShortcut(t, server, "comment", map[string]string{
"number": "1",
"body": "looks good",
})
if err != nil {
t.Fatalf("issue comment failed: %v", err)
}
assertEqual(t, commentPayload["notes"], "looks good")
}
func TestIssueAssignSendsCorrectUser(t *testing.T) {
var assignPayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
writeJSON(t, w, map[string]interface{}{
"subject": "Existing title",
"description": "Existing description",
})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
assignPayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"message": "指派成功"})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "assign", map[string]string{
"number": "42",
"user": "42",
})
if err != nil {
t.Fatalf("assign shortcut failed: %v", err)
}
// GitLink 责任人字段是 assigner_ids数组不是 assigned_to_id
got, ok := assignPayload["assigner_ids"].([]interface{})
if !ok || len(got) != 1 {
t.Fatalf("expected assigner_ids [42], got %#v", assignPayload["assigner_ids"])
}
assertEqual(t, got[0], float64(42))
}
func TestIssueAssignRequiresNumberAndUser(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
writeJSON(t, w, map[string]interface{}{
"message": "ok",
})
})
defer server.Close()
err := runIssueShortcut(t, server, "assign", map[string]string{"number": ""})
if err == nil {
t.Fatal("assign shortcut should error when required flags are missing")
}
}
func TestIssueLabelAddSendsCorrectTagIDs(t *testing.T) {
var capturedPaths []string
var capturedPayloads []map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/42/tags.json":
capturedPaths = append(capturedPaths, r.URL.Path)
capturedPayloads = append(capturedPayloads, decodeJSON(t, r))
writeJSON(t, w, map[string]interface{}{
"message": "标签添加成功",
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "label", map[string]string{
"number": "42",
"add": "5,8",
})
if err != nil {
t.Fatalf("label shortcut failed: %v", err)
}
if len(capturedPaths) != 2 {
t.Fatalf("expected 2 tag add calls, got %d", len(capturedPaths))
}
assertEqual(t, capturedPayloads[0]["tag_id"], "5")
assertEqual(t, capturedPayloads[1]["tag_id"], "8")
}
func TestIssueLabelRemoveSendsDeleteRequests(t *testing.T) {
var capturedPaths []string
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "DELETE" && r.URL.Path == "/v1/owner/repo/issues/42/tags/3.json":
capturedPaths = append(capturedPaths, r.URL.Path)
writeJSON(t, w, map[string]interface{}{
"message": "标签删除成功",
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "label", map[string]string{
"number": "42",
"remove": "3",
})
if err != nil {
t.Fatalf("label shortcut failed: %v", err)
}
if len(capturedPaths) != 1 {
t.Fatalf("expected 1 tag delete call, got %d", len(capturedPaths))
}
}
func TestBatchAssign(t *testing.T) {
var assignPayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && (r.URL.Path == "/v1/owner/repo/issues/1.json" || r.URL.Path == "/v1/owner/repo/issues/2.json"):
writeJSON(t, w, map[string]interface{}{"subject": "title", "description": "desc"})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/1.json":
assignPayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"message": "指派成功"})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/2.json":
writeJSON(t, w, map[string]interface{}{"message": "指派成功"})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "batch-assign", map[string]string{
"numbers": "1,2",
"user": "42",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-assign failed: %v", err)
}
got, ok := assignPayload["assigner_ids"].([]interface{})
if !ok || len(got) != 1 {
t.Fatalf("expected assigner_ids [42], got %#v", assignPayload["assigner_ids"])
}
assertEqual(t, got[0], float64(42))
}
func TestBatchAssignDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made in dry-run mode")
})
defer server.Close()
err := runIssueShortcut(t, server, "batch-assign", map[string]string{
"numbers": "1,2,3",
"user": "zhangsan",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-assign dry-run failed: %v", err)
}
}
func TestBatchLabelAddAndRemove(t *testing.T) {
var capturedPaths []string
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/1/tags.json":
capturedPaths = append(capturedPaths, r.URL.Path)
writeJSON(t, w, map[string]interface{}{"message": "标签添加成功"})
case r.Method == "DELETE" && r.URL.Path == "/v1/owner/repo/issues/1/tags/3.json":
capturedPaths = append(capturedPaths, r.URL.Path)
writeJSON(t, w, map[string]interface{}{"message": "标签删除成功"})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "batch-label", map[string]string{
"numbers": "1",
"add": "5",
"remove": "3",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-label failed: %v", err)
}
if len(capturedPaths) != 2 {
t.Fatalf("expected 2 API calls, got %d", len(capturedPaths))
}
}
func TestBatchLabelDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made in dry-run mode")
})
defer server.Close()
err := runIssueShortcut(t, server, "batch-label", map[string]string{
"numbers": "1,2",
"add": "5,8",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-label dry-run failed: %v", err)
}
}
func TestBatchMilestone(t *testing.T) {
var patchPayload map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/1.json":
writeJSON(t, w, map[string]interface{}{
"subject": "Test issue", "description": "desc",
})
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/1.json":
patchPayload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"message": "success"})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runIssueShortcut(t, server, "batch-milestone", map[string]string{
"numbers": "1",
"milestone": "5",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-milestone failed: %v", err)
}
assertEqual(t, patchPayload["fixed_version_id"], "5")
assertEqual(t, patchPayload["subject"], "Test issue")
assertEqual(t, patchPayload["description"], "desc")
}
func TestBatchMilestoneDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made in dry-run mode")
})
defer server.Close()
err := runIssueShortcut(t, server, "batch-milestone", map[string]string{
"numbers": "1,2,3",
"milestone": "5",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-milestone dry-run failed: %v", err)
}
}
func TestIssueLabelRequiresAddOrRemove(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("should not make any API calls")
})
defer server.Close()
err := runIssueShortcut(t, server, "label", map[string]string{
"number": "42",
})
if err == nil {
t.Fatal("expected error when neither --add nor --remove is provided")
}
}