gitlink-cli/shortcuts/issue/issue_test.go

831 lines
26 KiB
Go

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 TestIssueUpdatePreservesCurrentMetadata(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",
"priority": map[string]interface{}{"id": 2, "name": "normal"},
"tracker": map[string]interface{}{"id": 1, "name": "bug"},
"fixed_version": map[string]interface{}{"id": 9, "name": "v1"},
"assigned_to_id": 7,
"issue_tags": []map[string]interface{}{
{"id": 3, "name": "bug"},
{"id": 4, "name": "cli"},
},
})
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",
})
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["priority_id"], float64(2))
assertEqual(t, updatePayload["tracker_id"], float64(1))
assertEqual(t, updatePayload["fixed_version_id"], float64(9))
assertEqual(t, updatePayload["assigned_to_id"], float64(7))
tagIDs, ok := updatePayload["issue_tag_ids"].([]interface{})
if !ok {
t.Fatalf("issue_tag_ids = %T, want []interface{}", updatePayload["issue_tag_ids"])
}
assertEqual(t, tagIDs[0], float64(3))
assertEqual(t, tagIDs[1], float64(4))
}
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 TestIssueAssignersShortcutWithKeyword(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issue_assigners.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
assertEqual(t, r.URL.Query().Get("keyword"), "alice")
writeJSON(t, w, map[string]interface{}{
"total_count": 1,
"assigners": []map[string]interface{}{
{"id": 7, "name": "Alice", "login": "alice"},
},
})
})
defer server.Close()
err := runIssueShortcut(t, server, "assigners", map[string]string{
"keyword": "alice",
})
if err != nil {
t.Fatalf("assigners shortcut failed: %v", err)
}
}
func TestIssueAuthorsShortcutWithKeyword(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issue_authors.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
assertEqual(t, r.URL.Query().Get("keyword"), "bob")
writeJSON(t, w, map[string]interface{}{
"total_count": 1,
"authors": []map[string]interface{}{
{"id": 8, "name": "Bob", "login": "bob"},
},
})
})
defer server.Close()
err := runIssueShortcut(t, server, "authors", map[string]string{
"keyword": "bob",
})
if err != nil {
t.Fatalf("authors shortcut failed: %v", err)
}
}
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)
}
}
// --- batch-reopen ---
func TestBatchReopenPreservesCurrentDescription(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 := runShortcut(t, server, "batch-reopen", map[string]string{
"numbers": "42",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-reopen shortcut failed: %v", err)
}
assertEqual(t, updatePayload["subject"], "Existing title")
assertEqual(t, updatePayload["description"], "Existing description")
assertEqual(t, updatePayload["status_id"], float64(1))
}
func TestBatchReopenDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected in dry-run mode")
})
defer server.Close()
err := runShortcut(t, server, "batch-reopen", map[string]string{
"numbers": "1, 2, 3",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-reopen dry-run failed: %v", err)
}
}
func TestBatchReopenNoNumbers(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-reopen", map[string]string{})
if err == nil {
t.Fatal("expected error when no issue numbers provided")
}
}
func TestBatchReopenFetchFails(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
writeText(t, w, http.StatusNotFound, "not found")
})
defer server.Close()
err := runShortcut(t, server, "batch-reopen", map[string]string{"numbers": "99"})
if err == nil {
t.Fatal("expected error when fetch fails")
}
}
// --- batch-label ---
func TestBatchLabelAddLabels(t *testing.T) {
var patchPayloads []map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
// Handle GET requests to fetch issue data
if r.Method == "GET" {
writeJSON(t, w, map[string]interface{}{
"id": float64(1),
"tags": []interface{}{},
"issue_tags": []interface{}{},
})
return
}
// Handle PATCH request to update labels
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
payload := decodeJSON(t, r)
patchPayloads = append(patchPayloads, payload)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
})
defer server.Close()
err := runShortcut(t, server, "batch-label", map[string]string{
"ids": "1,2,3",
"add": "1,2",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-label shortcut failed: %v", err)
}
// Should have 3 PATCH requests (one per issue)
if len(patchPayloads) != 3 {
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
}
// Each PATCH should have a single issue ID and the new tags
for i, payload := range patchPayloads {
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
assertNumberSlice(t, payload["issue_tag_ids"], []float64{1, 2})
}
}
func TestBatchLabelRemoveLabels(t *testing.T) {
var patchPayloads []map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
// Handle GET requests to fetch issue data
if r.Method == "GET" {
writeJSON(t, w, map[string]interface{}{
"id": float64(1),
"tags": []interface{}{map[string]interface{}{"id": float64(1)}},
"issue_tags": []interface{}{map[string]interface{}{"id": float64(1)}},
})
return
}
// Handle PATCH request to update labels
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
payload := decodeJSON(t, r)
patchPayloads = append(patchPayloads, payload)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
})
defer server.Close()
err := runShortcut(t, server, "batch-label", map[string]string{
"ids": "1,2,3",
"remove": "1",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-label shortcut failed: %v", err)
}
// Should have 3 PATCH requests (one per issue)
if len(patchPayloads) != 3 {
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
}
// Each PATCH should have a single issue ID and empty tags (after removing tag 1)
for i, payload := range patchPayloads {
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
assertNumberSlice(t, payload["issue_tag_ids"], []float64{})
}
}
func TestBatchLabelDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected in dry-run mode")
})
defer server.Close()
err := runShortcut(t, server, "batch-label", map[string]string{
"ids": "1,2,3",
"add": "1",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-label dry-run failed: %v", err)
}
}
func TestBatchLabelNoIDs(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-label", map[string]string{"add": "1"})
if err == nil {
t.Fatal("expected error when no issue IDs provided")
}
}
func TestBatchLabelNoLabels(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-label", map[string]string{"ids": "1,2,3"})
if err == nil {
t.Fatal("expected error when no labels specified")
}
}
// --- batch-assign ---
func TestBatchAssignAddAssigners(t *testing.T) {
var patchPayloads []map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
// Handle GET requests to fetch issue data
if r.Method == "GET" {
writeJSON(t, w, map[string]interface{}{
"id": float64(1),
"assigners": []interface{}{},
})
return
}
// Handle PATCH request to update assigners
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
payload := decodeJSON(t, r)
patchPayloads = append(patchPayloads, payload)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
})
defer server.Close()
err := runShortcut(t, server, "batch-assign", map[string]string{
"ids": "1,2,3",
"add": "1,2",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-assign shortcut failed: %v", err)
}
// Should have 3 PATCH requests (one per issue)
if len(patchPayloads) != 3 {
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
}
// Each PATCH should have a single issue ID and the new assigners
for i, payload := range patchPayloads {
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
assertNumberSlice(t, payload["assigner_ids"], []float64{1, 2})
}
}
func TestBatchAssignRemoveAssigners(t *testing.T) {
var patchPayloads []map[string]interface{}
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
// Handle GET requests to fetch issue data
if r.Method == "GET" {
writeJSON(t, w, map[string]interface{}{
"id": float64(1),
"assigners": []interface{}{map[string]interface{}{"id": float64(1)}},
})
return
}
// Handle PATCH request to update assigners
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
payload := decodeJSON(t, r)
patchPayloads = append(patchPayloads, payload)
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
})
defer server.Close()
err := runShortcut(t, server, "batch-assign", map[string]string{
"ids": "1,2,3",
"remove": "1",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-assign shortcut failed: %v", err)
}
// Should have 3 PATCH requests (one per issue)
if len(patchPayloads) != 3 {
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
}
// Each PATCH should have a single issue ID and empty assigners (after removing assigner 1)
for i, payload := range patchPayloads {
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
assertNumberSlice(t, payload["assigner_ids"], []float64{})
}
}
func TestBatchAssignDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected in dry-run mode")
})
defer server.Close()
err := runShortcut(t, server, "batch-assign", map[string]string{
"ids": "1,2,3",
"add": "1",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-assign dry-run failed: %v", err)
}
}
func TestBatchAssignNoIDs(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-assign", map[string]string{"add": "1"})
if err == nil {
t.Fatal("expected error when no issue IDs provided")
}
}
func TestBatchAssignNoAssigners(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-assign", map[string]string{"ids": "1,2,3"})
if err == nil {
t.Fatal("expected error when no assigners specified")
}
}
// --- batch-comment ---
func TestBatchCommentAddsComments(t *testing.T) {
commentCount := 0
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": "Issue 1"})
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/2.json":
writeJSON(t, w, map[string]interface{}{"subject": "Issue 2"})
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/1/journals.json":
commentCount++
writeJSON(t, w, map[string]interface{}{"id": float64(1)})
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/2/journals.json":
commentCount++
writeJSON(t, w, map[string]interface{}{"id": float64(2)})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
})
defer server.Close()
err := runShortcut(t, server, "batch-comment", map[string]string{
"numbers": "1,2",
"message": "Batch comment",
"dry-run": "false",
})
if err != nil {
t.Fatalf("batch-comment shortcut failed: %v", err)
}
assertEqual(t, commentCount, 2)
}
func TestBatchCommentDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected in dry-run mode")
})
defer server.Close()
err := runShortcut(t, server, "batch-comment", map[string]string{
"numbers": "1,2,3",
"message": "Test comment",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-comment dry-run failed: %v", err)
}
}
func TestBatchCommentNoNumbers(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-comment", map[string]string{"message": "test"})
if err == nil {
t.Fatal("expected error when no issue numbers provided")
}
}
func TestBatchCommentNoBody(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-comment", map[string]string{"numbers": "1,2"})
if err == nil {
t.Fatal("expected error when no body provided")
}
}
// --- batch-export ---
func TestBatchExportToJSON(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" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
writeJSON(t, w, map[string]interface{}{
"issues": []interface{}{
map[string]interface{}{
"id": float64(1),
"subject": "Bug 1",
"status": map[string]interface{}{"id": float64(1), "name": "Open"},
"priority": map[string]interface{}{"id": float64(2), "name": "Normal"},
"assigners": []interface{}{},
"tags": []interface{}{},
"author": map[string]interface{}{"id": float64(1), "login": "alice"},
"created_on": "2026-01-01T00:00:00Z",
},
},
})
})
defer server.Close()
err := runShortcut(t, server, "batch-export", map[string]string{
"state": "open",
"limit": "100",
})
if err != nil {
t.Fatalf("batch-export shortcut failed: %v", err)
}
}
func TestBatchExportToCSV(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" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
writeJSON(t, w, map[string]interface{}{
"issues": []interface{}{
map[string]interface{}{
"id": float64(1),
"subject": "Bug 1",
"status": map[string]interface{}{"id": float64(1), "name": "Open"},
"priority": map[string]interface{}{"id": float64(2), "name": "Normal"},
"assigners": []interface{}{},
"tags": []interface{}{},
"author": map[string]interface{}{"id": float64(1), "login": "alice"},
"created_on": "2026-01-01T00:00:00Z",
},
},
})
})
defer server.Close()
err := runShortcut(t, server, "batch-export", map[string]string{
"state": "open",
"format": "csv",
"limit": "100",
})
if err != nil {
t.Fatalf("batch-export shortcut failed: %v", err)
}
}
func TestBatchExportWithFilters(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" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
query := r.URL.Query()
assertEqual(t, query.Get("category"), "closed")
assertEqual(t, query.Get("keyword"), "release")
assertEqual(t, query.Get("status_id"), "5")
writeJSON(t, w, map[string]interface{}{
"issues": []interface{}{
map[string]interface{}{
"id": float64(1),
"subject": "Filtered Issue",
"status": map[string]interface{}{"id": float64(5), "name": "Closed"},
"priority": map[string]interface{}{"id": float64(2), "name": "Normal"},
"assigners": []interface{}{},
"tags": []interface{}{},
"author": map[string]interface{}{"id": float64(1), "login": "alice"},
"created_on": "2026-01-01T00:00:00Z",
},
},
})
})
defer server.Close()
err := runShortcut(t, server, "batch-export", map[string]string{
"state": "closed",
"keyword": "release",
"status-id": "5",
"limit": "100",
})
if err != nil {
t.Fatalf("batch-export with filters failed: %v", err)
}
}
// --- batch-import ---
func TestBatchImportFromCSV(t *testing.T) {
createCount := 0
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/issues.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
payload := decodeJSON(t, r)
assertEqual(t, payload["subject"], "Imported Issue")
assertEqual(t, payload["status_id"], float64(1))
createCount++
writeJSON(t, w, map[string]interface{}{"id": float64(createCount)})
})
defer server.Close()
// Note: This test will fail because the file doesn't exist
// In a real test, we would create the file first
_ = runShortcut(t, server, "batch-import", map[string]string{
"file": "/tmp/test-import.csv",
"dry-run": "false",
})
_ = createCount // Avoid unused variable warning
}
func TestBatchImportDryRun(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected in dry-run mode")
})
defer server.Close()
// This test will fail because the file doesn't exist
// In a real test, we would create the file first
err := runShortcut(t, server, "batch-import", map[string]string{
"file": "/tmp/test-import.csv",
"dry-run": "true",
})
// We expect an error about missing file
if err == nil {
t.Fatal("expected error for missing file")
}
}
func TestBatchImportNoFile(t *testing.T) {
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected")
})
defer server.Close()
err := runShortcut(t, server, "batch-import", map[string]string{})
if err == nil {
t.Fatal("expected error when no file provided")
}
}