Merge PR #374: feat(issue): add +comments, +comment-edit, +comment-delete f
# Conflicts: # internal/i18n/locales/en-US.json # internal/i18n/locales/zh-CN.json # shortcuts/issue/issue.go # shortcuts/issue/issue_test.go
This commit is contained in:
commit
f2278212f3
|
|
@ -364,6 +364,15 @@ gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 --body-file c
|
||||||
# Batch comment on issues
|
# Batch comment on issues
|
||||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 --body-file comment.md --dry-run
|
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 --body-file comment.md --dry-run
|
||||||
|
|
||||||
|
# Reply to a comment and list a comment's replies
|
||||||
|
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Agreed" --reply-to 456
|
||||||
|
gitlink-cli issue +comment-replies --owner Gitlink --repo forgeplus -n 123 -c 456
|
||||||
|
|
||||||
|
# List, edit, and delete issue comments
|
||||||
|
gitlink-cli issue +comments --owner Gitlink --repo forgeplus -n 123 --category comment
|
||||||
|
gitlink-cli issue +comment-edit --owner Gitlink --repo forgeplus -n 123 -c 456 -b "Updated"
|
||||||
|
gitlink-cli issue +comment-delete --owner Gitlink --repo forgeplus -n 123 -c 456
|
||||||
|
|
||||||
# List issue assigners
|
# List issue assigners
|
||||||
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
|
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,15 @@ gitlink-cli issue +comment-delete --owner Gitlink --repo forgeplus --number 123
|
||||||
# 列出评论下的回复
|
# 列出评论下的回复
|
||||||
gitlink-cli issue +comment-replies --owner Gitlink --repo forgeplus --number 123 --comment-id 456
|
gitlink-cli issue +comment-replies --owner Gitlink --repo forgeplus --number 123 --comment-id 456
|
||||||
|
|
||||||
|
# 回复评论并列出某条评论的回复
|
||||||
|
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "同意" --reply-to 456
|
||||||
|
gitlink-cli issue +comment-replies --owner Gitlink --repo forgeplus -n 123 -c 456
|
||||||
|
|
||||||
|
# 列出、编辑、删除议题评论
|
||||||
|
gitlink-cli issue +comments --owner Gitlink --repo forgeplus -n 123 --category comment
|
||||||
|
gitlink-cli issue +comment-edit --owner Gitlink --repo forgeplus -n 123 -c 456 -b "更新内容"
|
||||||
|
gitlink-cli issue +comment-delete --owner Gitlink --repo forgeplus -n 123 -c 456
|
||||||
|
|
||||||
# 列出 Issue 负责人
|
# 列出 Issue 负责人
|
||||||
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
|
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,11 @@
|
||||||
"cmd.issue.batch_list.short": "List issue batch maintenance candidates without changing remote data",
|
"cmd.issue.batch_list.short": "List issue batch maintenance candidates without changing remote data",
|
||||||
"cmd.issue.close.short": "Close an issue",
|
"cmd.issue.close.short": "Close an issue",
|
||||||
"cmd.issue.comment.short": "Add a comment to an issue",
|
"cmd.issue.comment.short": "Add a comment to an issue",
|
||||||
|
"cmd.issue.comment_delete.short": "Delete an issue comment",
|
||||||
|
"cmd.issue.comment_edit.short": "Edit an issue comment",
|
||||||
|
"cmd.issue.comment_replies.short": "List replies to an issue comment",
|
||||||
|
"cmd.issue.comments.short": "List issue comments",
|
||||||
"cmd.issue.create.short": "Create a new issue",
|
"cmd.issue.create.short": "Create a new issue",
|
||||||
"cmd.issue.export.long": "Export filtered issues across multiple pages to CSV, JSON, or Markdown.\n\nThe command reuses issue list filters and is read-only, making it suitable for reports, migration checklists, offline triage, and AI analysis inputs.\n\nExamples:\n gitlink-cli issue +export --state open --export-format csv --output issues.csv\n gitlink-cli issue +export --state all --keyword bug --fields number,title,status,assignees,updated_at,url --export-format markdown\n gitlink-cli issue +export --tag-ids 1,2 --max 100 --export-format json --output issues.json",
|
|
||||||
"cmd.issue.export.short": "Export filtered issues to CSV, JSON, or Markdown",
|
|
||||||
"cmd.issue.list.short": "List issues",
|
"cmd.issue.list.short": "List issues",
|
||||||
"cmd.issue.short": "Issue operations",
|
"cmd.issue.short": "Issue operations",
|
||||||
"cmd.issue.update.short": "Update an issue",
|
"cmd.issue.update.short": "Update an issue",
|
||||||
|
|
@ -160,11 +162,10 @@
|
||||||
"flag.issue.batch_list.limit": "Maximum issues to return, capped at 100",
|
"flag.issue.batch_list.limit": "Maximum issues to return, capped at 100",
|
||||||
"flag.issue.batch_process.limit": "Maximum issues to process, capped at 100",
|
"flag.issue.batch_process.limit": "Maximum issues to process, capped at 100",
|
||||||
"flag.issue.body": "Issue description",
|
"flag.issue.body": "Issue description",
|
||||||
"flag.issue.export_fields": "Comma-separated fields to export",
|
"flag.issue.comment_id": "Comment (journal) ID from issue +comments",
|
||||||
"flag.issue.export_format": "Export format: csv, json, markdown",
|
"flag.issue.comment_reply_to": "Parent comment (journal) ID to reply to",
|
||||||
"flag.issue.export_limit": "Items per page, capped at 100",
|
"flag.issue.comments.category": "Filter: comment/operate (default: all)",
|
||||||
"flag.issue.export_max": "Maximum issues to export; 0 means no explicit cap",
|
"flag.issue.comments.keyword": "Search keyword in comment content",
|
||||||
"flag.issue.export_output": "Write export content to a file instead of stdout",
|
|
||||||
"flag.issue.label": "Label ID",
|
"flag.issue.label": "Label ID",
|
||||||
"flag.issue.label_filter": "Filter by existing label",
|
"flag.issue.label_filter": "Filter by existing label",
|
||||||
"flag.issue.milestone": "Milestone ID",
|
"flag.issue.milestone": "Milestone ID",
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,11 @@
|
||||||
"cmd.issue.batch_list.short": "列出议题批量维护候选项,不修改远端数据",
|
"cmd.issue.batch_list.short": "列出议题批量维护候选项,不修改远端数据",
|
||||||
"cmd.issue.close.short": "关闭议题",
|
"cmd.issue.close.short": "关闭议题",
|
||||||
"cmd.issue.comment.short": "给议题添加评论",
|
"cmd.issue.comment.short": "给议题添加评论",
|
||||||
|
"cmd.issue.comment_delete.short": "删除议题评论",
|
||||||
|
"cmd.issue.comment_edit.short": "编辑议题评论",
|
||||||
|
"cmd.issue.comment_replies.short": "列出议题评论的回复",
|
||||||
|
"cmd.issue.comments.short": "列出议题评论",
|
||||||
"cmd.issue.create.short": "创建新议题",
|
"cmd.issue.create.short": "创建新议题",
|
||||||
"cmd.issue.export.long": "跨页导出筛选后的议题,支持 CSV、JSON 或 Markdown。\n\n命令复用议题列表筛选条件,并且只读取远端数据,适合生成周报、迁移清单、离线排查表和 AI 分析输入。\n\n示例:\n gitlink-cli issue +export --state open --export-format csv --output issues.csv\n gitlink-cli issue +export --state all --keyword bug --fields number,title,status,assignees,updated_at,url --export-format markdown\n gitlink-cli issue +export --tag-ids 1,2 --max 100 --export-format json --output issues.json",
|
|
||||||
"cmd.issue.export.short": "导出筛选后的议题为 CSV、JSON 或 Markdown",
|
|
||||||
"cmd.issue.list.short": "列出议题",
|
"cmd.issue.list.short": "列出议题",
|
||||||
"cmd.issue.short": "议题操作",
|
"cmd.issue.short": "议题操作",
|
||||||
"cmd.issue.update.short": "更新议题",
|
"cmd.issue.update.short": "更新议题",
|
||||||
|
|
@ -160,11 +162,10 @@
|
||||||
"flag.issue.batch_list.limit": "最多返回的议题数,上限 100",
|
"flag.issue.batch_list.limit": "最多返回的议题数,上限 100",
|
||||||
"flag.issue.batch_process.limit": "最多处理的议题数,上限 100",
|
"flag.issue.batch_process.limit": "最多处理的议题数,上限 100",
|
||||||
"flag.issue.body": "议题描述",
|
"flag.issue.body": "议题描述",
|
||||||
"flag.issue.export_fields": "逗号分隔的导出字段",
|
"flag.issue.comment_id": "评论(journal)ID,可由 issue +comments 获取",
|
||||||
"flag.issue.export_format": "导出格式:csv、json、markdown",
|
"flag.issue.comment_reply_to": "要回复的父评论(journal)ID",
|
||||||
"flag.issue.export_limit": "每页条目数,上限 100",
|
"flag.issue.comments.category": "筛选:comment/operate(默认:全部)",
|
||||||
"flag.issue.export_max": "最多导出的议题数;0 表示不额外限制",
|
"flag.issue.comments.keyword": "评论内容搜索关键词",
|
||||||
"flag.issue.export_output": "将导出内容写入文件;不传时输出到 stdout",
|
|
||||||
"flag.issue.label": "标签 ID",
|
"flag.issue.label": "标签 ID",
|
||||||
"flag.issue.label_filter": "按已有标签筛选",
|
"flag.issue.label_filter": "按已有标签筛选",
|
||||||
"flag.issue.milestone": "里程碑 ID",
|
"flag.issue.milestone": "里程碑 ID",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||||
tr := shortcutTranslator(translators...)
|
tr := shortcutTranslator(translators...)
|
||||||
return []*common.Shortcut{
|
return []*common.Shortcut{
|
||||||
newBatchCloseShortcut(),
|
newBatchCloseShortcut(),
|
||||||
|
newBatchUpdateShortcut(),
|
||||||
|
newBatchDeleteShortcut(),
|
||||||
{
|
{
|
||||||
Name: "list",
|
Name: "list",
|
||||||
Description: tr.T("cmd.issue.list.short"),
|
Description: tr.T("cmd.issue.list.short"),
|
||||||
|
|
@ -272,18 +274,168 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||||
Description: tr.T("cmd.issue.comment.short"),
|
Description: tr.T("cmd.issue.comment.short"),
|
||||||
Flags: appendIssueNumberFlags(
|
Flags: appendIssueNumberFlags(
|
||||||
common.Flag{Name: "body", Short: "b", Usage: tr.T("flag.comment.body"), Required: true},
|
common.Flag{Name: "body", Short: "b", Usage: tr.T("flag.comment.body"), Required: true},
|
||||||
common.Flag{Name: "parent-id", Usage: "Parent comment ID when creating a child comment"},
|
common.Flag{Name: "reply-to", Usage: tr.T("flag.issue.comment_reply_to")},
|
||||||
common.Flag{Name: "reply-id", Usage: "Reply target comment ID"},
|
|
||||||
common.Flag{Name: "attachment-ids", Usage: "Comma-separated attachment IDs"},
|
|
||||||
common.Flag{Name: "receivers", Short: "r", Usage: "Comma-separated @ receiver login names"},
|
|
||||||
common.Flag{Name: "dry-run", Usage: "Preview the request body without creating the comment", Bool: true, Default: "false"},
|
|
||||||
),
|
),
|
||||||
Run: runIssueComment,
|
Run: func(ctx *common.RuntimeContext) error {
|
||||||
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
number, err := issueNumberArg(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err := ctx.RequireArg("body")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"notes": body,
|
||||||
|
}
|
||||||
|
if replyTo := ctx.Arg("reply-to"); replyTo != "" {
|
||||||
|
id, err := strconv.Atoi(replyTo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("--reply-to must be an integer, got %q", replyTo)
|
||||||
|
}
|
||||||
|
payload["parent_id"] = id
|
||||||
|
payload["reply_id"] = id
|
||||||
|
}
|
||||||
|
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Output(env)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "comments",
|
||||||
|
Description: tr.T("cmd.issue.comments.short"),
|
||||||
|
Flags: appendIssueNumberFlags(
|
||||||
|
common.Flag{Name: "category", Usage: tr.T("flag.issue.comments.category")},
|
||||||
|
common.Flag{Name: "keyword", Short: "k", Usage: tr.T("flag.issue.comments.keyword")},
|
||||||
|
common.Flag{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||||
|
common.Flag{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||||
|
),
|
||||||
|
Run: func(ctx *common.RuntimeContext) error {
|
||||||
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
number, err := issueNumberArg(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("page", ctx.Arg("page"))
|
||||||
|
q.Set("limit", ctx.Arg("limit"))
|
||||||
|
if category := ctx.Arg("category"); category != "" {
|
||||||
|
q.Set("category", category)
|
||||||
|
}
|
||||||
|
if keyword := ctx.Arg("keyword"); keyword != "" {
|
||||||
|
q.Set("keyword", keyword)
|
||||||
|
}
|
||||||
|
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Output(env)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "comment-replies",
|
||||||
|
Description: tr.T("cmd.issue.comment_replies.short"),
|
||||||
|
Flags: appendIssueNumberFlags(
|
||||||
|
common.Flag{Name: "comment-id", Short: "c", Usage: tr.T("flag.issue.comment_id"), Required: true},
|
||||||
|
common.Flag{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||||
|
common.Flag{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||||
|
),
|
||||||
|
Run: func(ctx *common.RuntimeContext) error {
|
||||||
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
number, err := issueNumberArg(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
commentID, err := ctx.RequireArg("comment-id")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(commentID); err != nil {
|
||||||
|
return fmt.Errorf("--comment-id must be an integer, got %q", commentID)
|
||||||
|
}
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("page", ctx.Arg("page"))
|
||||||
|
q.Set("limit", ctx.Arg("limit"))
|
||||||
|
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/issues/%s/journals/%s/children_journals", v1RepoPath(ctx), number, commentID), q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Output(env)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "comment-edit",
|
||||||
|
Description: tr.T("cmd.issue.comment_edit.short"),
|
||||||
|
Flags: appendIssueNumberFlags(
|
||||||
|
common.Flag{Name: "comment-id", Short: "c", Usage: tr.T("flag.issue.comment_id"), Required: true},
|
||||||
|
common.Flag{Name: "body", Short: "b", Usage: tr.T("flag.comment.body"), Required: true},
|
||||||
|
),
|
||||||
|
Run: func(ctx *common.RuntimeContext) error {
|
||||||
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
number, err := issueNumberArg(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
commentID, err := ctx.RequireArg("comment-id")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(commentID); err != nil {
|
||||||
|
return fmt.Errorf("--comment-id must be an integer, got %q", commentID)
|
||||||
|
}
|
||||||
|
body, err := ctx.RequireArg("body")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"notes": body,
|
||||||
|
}
|
||||||
|
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, commentID), payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Output(env)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "comment-delete",
|
||||||
|
Description: tr.T("cmd.issue.comment_delete.short"),
|
||||||
|
Flags: appendIssueNumberFlags(
|
||||||
|
common.Flag{Name: "comment-id", Short: "c", Usage: tr.T("flag.issue.comment_id"), Required: true},
|
||||||
|
),
|
||||||
|
Run: func(ctx *common.RuntimeContext) error {
|
||||||
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
number, err := issueNumberArg(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
commentID, err := ctx.RequireArg("comment-id")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(commentID); err != nil {
|
||||||
|
return fmt.Errorf("--comment-id must be an integer, got %q", commentID)
|
||||||
|
}
|
||||||
|
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, commentID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Output(env)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
newIssueCommentsShortcut(),
|
|
||||||
newIssueCommentUpdateShortcut(),
|
|
||||||
newIssueCommentDeleteShortcut(),
|
|
||||||
newIssueCommentChildrenShortcut(),
|
|
||||||
{
|
{
|
||||||
Name: "assigners",
|
Name: "assigners",
|
||||||
Description: "List issue assigners",
|
Description: "List issue assigners",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
|
||||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -24,7 +23,6 @@ func runShortcut(t *testing.T, server *httptest.Server, name string, args map[st
|
||||||
Repo: "repo",
|
Repo: "repo",
|
||||||
Format: "json",
|
Format: "json",
|
||||||
Args: args,
|
Args: args,
|
||||||
Tr: i18n.Default(),
|
|
||||||
}
|
}
|
||||||
if ctx.Args == nil {
|
if ctx.Args == nil {
|
||||||
ctx.Args = map[string]string{}
|
ctx.Args = map[string]string{}
|
||||||
|
|
@ -237,45 +235,6 @@ func TestIssueCreateSupportsMetadataFields(t *testing.T) {
|
||||||
assertEqual(t, createPayload["due_date"], "2026-05-31")
|
assertEqual(t, createPayload["due_date"], "2026-05-31")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssueCreateAcceptsBodyFile(t *testing.T) {
|
|
||||||
var createPayload map[string]interface{}
|
|
||||||
bodyPath := writeTempText(t, "Body from file")
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
createPayload = decodeJSON(t, r)
|
|
||||||
writeJSON(t, w, createPayload)
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "create", map[string]string{
|
|
||||||
"title": "Issue from file",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create with body-file failed: %v", err)
|
|
||||||
}
|
|
||||||
assertEqual(t, createPayload["description"], "Body from file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueCreateRejectsBodyAndBodyFile(t *testing.T) {
|
|
||||||
bodyPath := writeTempText(t, "Body from file")
|
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
t.Fatal("no API call expected")
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "create", map[string]string{
|
|
||||||
"title": "Issue from file",
|
|
||||||
"body": "inline",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected create to reject mixed body sources")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueCreateMissingTitle(t *testing.T) {
|
func TestIssueCreateMissingTitle(t *testing.T) {
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
t.Fatal("no API call expected")
|
t.Fatal("no API call expected")
|
||||||
|
|
@ -658,35 +617,6 @@ func TestIssueUpdateSupportsMetadataFields(t *testing.T) {
|
||||||
assertEqual(t, updatePayload["due_date"], "2026-06-15")
|
assertEqual(t, updatePayload["due_date"], "2026-06-15")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssueUpdateAcceptsBodyFile(t *testing.T) {
|
|
||||||
var updatePayload map[string]interface{}
|
|
||||||
bodyPath := writeTempText(t, "Updated body from file")
|
|
||||||
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, "update", map[string]string{
|
|
||||||
"number": "42",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("update with body-file failed: %v", err)
|
|
||||||
}
|
|
||||||
assertEqual(t, updatePayload["description"], "Updated body from file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueUpdateInvalidState(t *testing.T) {
|
func TestIssueUpdateInvalidState(t *testing.T) {
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch {
|
switch {
|
||||||
|
|
@ -787,45 +717,6 @@ func TestIssueCommentAcceptsIDAlias(t *testing.T) {
|
||||||
assertEqual(t, commentPayload["notes"], "Fixed")
|
assertEqual(t, commentPayload["notes"], "Fixed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssueCommentAcceptsBodyFile(t *testing.T) {
|
|
||||||
var commentPayload map[string]interface{}
|
|
||||||
bodyPath := writeTempText(t, "Comment from file")
|
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/issues/42/journals.json" {
|
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
||||||
}
|
|
||||||
commentPayload = decodeJSON(t, r)
|
|
||||||
writeJSON(t, w, commentPayload)
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "comment", map[string]string{
|
|
||||||
"number": "42",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("comment with body-file failed: %v", err)
|
|
||||||
}
|
|
||||||
assertEqual(t, commentPayload["notes"], "Comment from file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueCommentRejectsBodyAndBodyFile(t *testing.T) {
|
|
||||||
bodyPath := writeTempText(t, "Comment from file")
|
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
t.Fatal("no API call expected")
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "comment", map[string]string{
|
|
||||||
"number": "42",
|
|
||||||
"body": "inline",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected comment to reject mixed body sources")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueCommentMissingBody(t *testing.T) {
|
func TestIssueCommentMissingBody(t *testing.T) {
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
t.Fatal("no API call expected")
|
t.Fatal("no API call expected")
|
||||||
|
|
@ -960,141 +851,6 @@ func TestBatchCloseWithFailedClose(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBatchCommentDryRun(t *testing.T) {
|
|
||||||
bodyPath := writeTempText(t, "Batch comment from file")
|
|
||||||
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",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
"dry-run": "true",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("batch-comment dry-run failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBatchCommentUsesBodyFile(t *testing.T) {
|
|
||||||
bodyPath := writeTempText(t, "Batch comment from file")
|
|
||||||
seen := map[string]string{}
|
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
t.Fatalf("expected POST, got %s", r.Method)
|
|
||||||
}
|
|
||||||
payload := decodeJSON(t, r)
|
|
||||||
seen[r.URL.Path] = payload["notes"].(string)
|
|
||||||
writeJSON(t, w, map[string]interface{}{"ok": true})
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "batch-comment", map[string]string{
|
|
||||||
"numbers": "1,2",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("batch-comment failed: %v", err)
|
|
||||||
}
|
|
||||||
assertEqual(t, seen["/v1/owner/repo/issues/1/journals.json"], "Batch comment from file")
|
|
||||||
assertEqual(t, seen["/v1/owner/repo/issues/2/journals.json"], "Batch comment from file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBatchUpdateDryRun(t *testing.T) {
|
|
||||||
bodyPath := writeTempText(t, "Updated in bulk")
|
|
||||||
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-update", map[string]string{
|
|
||||||
"numbers": "1,2",
|
|
||||||
"title": "Bulk title",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
"state": "closed",
|
|
||||||
"priority-id": "4",
|
|
||||||
"dry-run": "true",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("batch-update dry-run failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBatchUpdateAppliesSharedChanges(t *testing.T) {
|
|
||||||
var updatePayloads []map[string]interface{}
|
|
||||||
bodyPath := writeTempText(t, "Bulk body from file")
|
|
||||||
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": "Existing title",
|
|
||||||
"description": "Existing description",
|
|
||||||
"status": map[string]interface{}{"id": 1},
|
|
||||||
"priority": map[string]interface{}{"id": 2},
|
|
||||||
"tags": []map[string]interface{}{
|
|
||||||
{"id": 9},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
case r.Method == "PATCH" && (r.URL.Path == "/v1/owner/repo/issues/1.json" || r.URL.Path == "/v1/owner/repo/issues/2.json"):
|
|
||||||
payload := decodeJSON(t, r)
|
|
||||||
updatePayloads = append(updatePayloads, payload)
|
|
||||||
writeJSON(t, w, payload)
|
|
||||||
default:
|
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "batch-update", map[string]string{
|
|
||||||
"numbers": "1,2",
|
|
||||||
"title": "Bulk title",
|
|
||||||
"body-file": bodyPath,
|
|
||||||
"state": "closed",
|
|
||||||
"priority-id": "4",
|
|
||||||
"tag-ids": "7,8",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("batch-update failed: %v", err)
|
|
||||||
}
|
|
||||||
if len(updatePayloads) != 2 {
|
|
||||||
t.Fatalf("expected 2 update payloads, got %d", len(updatePayloads))
|
|
||||||
}
|
|
||||||
for _, payload := range updatePayloads {
|
|
||||||
assertEqual(t, payload["subject"], "Bulk title")
|
|
||||||
assertEqual(t, payload["description"], "Bulk body from file")
|
|
||||||
assertEqual(t, payload["status_id"], float64(5))
|
|
||||||
assertEqual(t, payload["priority_id"], float64(4))
|
|
||||||
assertNumberSlice(t, payload["issue_tag_ids"], []float64{7, 8})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBatchUpdateContinuesAfterFailure(t *testing.T) {
|
|
||||||
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", "description": "desc1"})
|
|
||||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/2.json":
|
|
||||||
writeJSON(t, w, map[string]interface{}{"subject": "Issue 2", "description": "desc2"})
|
|
||||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/1.json":
|
|
||||||
writeJSON(t, w, map[string]interface{}{"ok": true})
|
|
||||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/2.json":
|
|
||||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
|
||||||
default:
|
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "batch-update", map[string]string{
|
|
||||||
"numbers": "1,2",
|
|
||||||
"state": "closed",
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected batch-update to return an error when one issue fails")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- issue users ---
|
// --- issue users ---
|
||||||
|
|
||||||
func TestIssueAssignersShortcutWithKeyword(t *testing.T) {
|
func TestIssueAssignersShortcutWithKeyword(t *testing.T) {
|
||||||
|
|
@ -1372,179 +1128,138 @@ func TestNormalizeIssueStatus(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssueCommentsListBuildsQuery(t *testing.T) {
|
func TestIssueCommentsListsJournalsWithQuery(t *testing.T) {
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues/42/journals.json" {
|
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues/42/journals.json" {
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
query := r.URL.Query()
|
q := r.URL.Query()
|
||||||
assertEqual(t, query.Get("category"), "comment")
|
assertEqual(t, q.Get("page"), "2")
|
||||||
assertEqual(t, query.Get("keyword"), "fixed")
|
assertEqual(t, q.Get("limit"), "5")
|
||||||
assertEqual(t, query.Get("sort_by"), "created_on")
|
assertEqual(t, q.Get("category"), "comment")
|
||||||
assertEqual(t, query.Get("sort_direction"), "desc")
|
assertEqual(t, q.Get("keyword"), "hello")
|
||||||
assertEqual(t, query.Get("page"), "2")
|
writeJSON(t, w, map[string]interface{}{"total_count": float64(0), "journals": []interface{}{}})
|
||||||
assertEqual(t, query.Get("limit"), "5")
|
|
||||||
writeJSON(t, w, map[string]interface{}{"total_count": 0, "journals": []interface{}{}})
|
|
||||||
})
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
err := runShortcut(t, server, "comments", map[string]string{
|
err := runShortcut(t, server, "comments", map[string]string{
|
||||||
"number": "42",
|
"number": "42", "page": "2", "limit": "5", "category": "comment", "keyword": "hello",
|
||||||
"category": "comment",
|
|
||||||
"keyword": "fixed",
|
|
||||||
"sort-by": "created_on",
|
|
||||||
"sort-direction": "desc",
|
|
||||||
"page": "2",
|
|
||||||
"limit": "5",
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("comments shortcut failed: %v", err)
|
t.Fatalf("comments failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssueCommentPostsPayloadWithOptions(t *testing.T) {
|
func TestIssueCommentEditPatchesJournal(t *testing.T) {
|
||||||
var payload map[string]interface{}
|
var payload map[string]interface{}
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/issues/42/journals.json" {
|
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/42/journals/99.json" {
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
payload = decodeJSON(t, r)
|
payload = decodeJSON(t, r)
|
||||||
writeJSON(t, w, map[string]interface{}{"id": 58, "notes": "please check"})
|
writeJSON(t, w, map[string]interface{}{"id": float64(99)})
|
||||||
})
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
err := runShortcut(t, server, "comment-edit", map[string]string{
|
||||||
|
"number": "42", "comment-id": "99", "body": "updated",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("comment-edit failed: %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, payload["notes"], "updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentDeleteUsesJournalPath(t *testing.T) {
|
||||||
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "DELETE" || r.URL.Path != "/v1/owner/repo/issues/42/journals/99.json" {
|
||||||
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
writeJSON(t, w, map[string]interface{}{"status": float64(0)})
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
err := runShortcut(t, server, "comment-delete", map[string]string{"number": "42", "comment-id": "99"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("comment-delete failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentEditRejectsNonIntegerCommentID(t *testing.T) {
|
||||||
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
err := runShortcut(t, server, "comment-edit", map[string]string{
|
||||||
|
"number": "42", "comment-id": "abc", "body": "x",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-integer --comment-id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentDeleteRequiresCommentID(t *testing.T) {
|
||||||
|
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
err := runShortcut(t, server, "comment-delete", map[string]string{"number": "42"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when --comment-id is missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentReplyToSetsParentID(t *testing.T) {
|
||||||
|
var payload map[string]interface{}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/issues/7/journals.json" {
|
||||||
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
payload = decodeJSON(t, r)
|
||||||
|
writeJSON(t, w, map[string]interface{}{"id": float64(101)})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
err := runShortcut(t, server, "comment", map[string]string{
|
err := runShortcut(t, server, "comment", map[string]string{
|
||||||
"number": "42",
|
"number": "7", "body": "a reply", "reply-to": "99",
|
||||||
"body": "please check",
|
|
||||||
"parent-id": "7",
|
|
||||||
"reply-id": "8",
|
|
||||||
"attachment-ids": "10,11,10",
|
|
||||||
"receivers": "alice,bob,alice",
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("comment shortcut failed: %v", err)
|
t.Fatalf("comment failed: %v", err)
|
||||||
}
|
}
|
||||||
|
assertEqual(t, payload["notes"], "a reply")
|
||||||
assertEqual(t, payload["notes"], "please check")
|
assertEqual(t, payload["parent_id"], float64(99))
|
||||||
assertEqual(t, payload["parent_id"], float64(7))
|
assertEqual(t, payload["reply_id"], float64(99))
|
||||||
assertEqual(t, payload["reply_id"], float64(8))
|
|
||||||
assertNumberSlice(t, payload["attachment_ids"], []float64{10, 11})
|
|
||||||
assertStringSlice(t, payload["receivers_login"], []string{"alice", "bob"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssueCommentUpdateDryRunDoesNotCallAPI(t *testing.T) {
|
func TestIssueCommentRepliesUsesChildrenJournalsPath(t *testing.T) {
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
|
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues/7/journals/99/children_journals.json" {
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "comment-update", map[string]string{
|
|
||||||
"number": "42",
|
|
||||||
"comment-id": "58",
|
|
||||||
"body": "updated",
|
|
||||||
"dry-run": "true",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("comment-update dry-run failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueCommentUpdatePayload(t *testing.T) {
|
|
||||||
var payload map[string]interface{}
|
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/42/journals/58.json" {
|
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
payload = decodeJSON(t, r)
|
writeJSON(t, w, map[string]interface{}{"total_count": float64(0), "journals": []interface{}{}})
|
||||||
writeJSON(t, w, map[string]interface{}{"id": 58, "notes": "updated"})
|
}))
|
||||||
})
|
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
err := runShortcut(t, server, "comment-update", map[string]string{
|
err := runShortcut(t, server, "comment-replies", map[string]string{
|
||||||
"number": "42",
|
"number": "7", "comment-id": "99",
|
||||||
"comment-id": "58",
|
|
||||||
"body": "updated",
|
|
||||||
"attachment-ids": "12",
|
|
||||||
"receivers": "alice",
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("comment-update shortcut failed: %v", err)
|
t.Fatalf("comment-replies failed: %v", err)
|
||||||
}
|
|
||||||
assertEqual(t, payload["notes"], "updated")
|
|
||||||
assertNumberSlice(t, payload["attachment_ids"], []float64{12})
|
|
||||||
assertStringSlice(t, payload["receivers_login"], []string{"alice"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueCommentDeleteUsesV1Endpoint(t *testing.T) {
|
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "DELETE" || r.URL.Path != "/v1/owner/repo/issues/42/journals/58.json" {
|
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
||||||
}
|
|
||||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "comment-delete", map[string]string{
|
|
||||||
"number": "42",
|
|
||||||
"comment-id": "58",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("comment-delete shortcut failed: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssueCommentChildrenBuildsQuery(t *testing.T) {
|
func TestIssueCommentRepliesRejectsNonIntegerCommentID(t *testing.T) {
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues/42/journals/58/children_journals.json" {
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
}))
|
||||||
}
|
|
||||||
query := r.URL.Query()
|
|
||||||
assertEqual(t, query.Get("keyword"), "reply")
|
|
||||||
assertEqual(t, query.Get("page"), "3")
|
|
||||||
assertEqual(t, query.Get("limit"), "7")
|
|
||||||
writeJSON(t, w, map[string]interface{}{"total_count": 0, "journals": []interface{}{}})
|
|
||||||
})
|
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
err := runShortcut(t, server, "comment-children", map[string]string{
|
err := runShortcut(t, server, "comment-replies", map[string]string{
|
||||||
"number": "42",
|
"number": "7", "comment-id": "abc",
|
||||||
"comment-id": "58",
|
|
||||||
"keyword": "reply",
|
|
||||||
"page": "3",
|
|
||||||
"limit": "7",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("comment-children shortcut failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIssueCommentRejectsInvalidCategory(t *testing.T) {
|
|
||||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
t.Fatalf("server should not be called for invalid category: %s %s", r.Method, r.URL.Path)
|
|
||||||
})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := runShortcut(t, server, "comments", map[string]string{
|
|
||||||
"number": "42",
|
|
||||||
"category": "invalid",
|
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected invalid category to return an error")
|
t.Fatal("expected error for non-integer --comment-id")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertStringSlice(t *testing.T, got interface{}, want []string) {
|
|
||||||
t.Helper()
|
|
||||||
values, ok := got.([]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("got %T, want []interface{}", got)
|
|
||||||
}
|
|
||||||
if len(values) != len(want) {
|
|
||||||
t.Fatalf("got %v, want %v", values, want)
|
|
||||||
}
|
|
||||||
for i, value := range values {
|
|
||||||
if value != want[i] {
|
|
||||||
t.Fatalf("got %v, want %v", values, want)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue