9de9dffab1
- add issue close and issue reopen subcommands - add GetIssue and SetIssueState to the Gitea client - fail when the issue is already in the requested state - document the new subcommands in README.md and AGENTS.md
205 lines
7.2 KiB
Go
205 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// A malformed --repo must fail the command (so main exits non-zero). ParseRepo
|
|
// rejects it before any Vault/Gitea call, so this stays hermetic.
|
|
func TestExecuteBadRepoErrors(t *testing.T) {
|
|
cmd := newRootCmd()
|
|
cmd.SetArgs([]string{"pr", "create", "--repo", "not-a-repo", "--base", "main", "--head", "x", "--title", "t"})
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
if err := cmd.Execute(); err == nil {
|
|
t.Fatal("Execute() = nil, want error for a malformed --repo")
|
|
}
|
|
}
|
|
|
|
// `pr edit` with neither --title nor --body has nothing to send; it must fail
|
|
// with a usage error before any Vault/Gitea call, so this stays hermetic.
|
|
func TestPREditRequiresTitleOrBody(t *testing.T) {
|
|
cmd := newRootCmd()
|
|
cmd.SetArgs([]string{"pr", "edit", "--repo", "unkin/repo", "--pr", "7"})
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error when neither --title nor --body is given")
|
|
}
|
|
if !strings.Contains(err.Error(), "--title or --body") {
|
|
t.Errorf("error = %q, want it to name the missing flags", err)
|
|
}
|
|
}
|
|
|
|
// Gitea silently ignores an empty title, so `pr edit --title ""` would report
|
|
// success while changing nothing; it must fail before any Vault/Gitea call.
|
|
func TestPREditRejectsEmptyTitle(t *testing.T) {
|
|
cmd := newRootCmd()
|
|
cmd.SetArgs([]string{"pr", "edit", "--repo", "unkin/repo", "--pr", "7", "--title", ""})
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error for an empty --title")
|
|
}
|
|
if !strings.Contains(err.Error(), "--title cannot be empty") {
|
|
t.Errorf("error = %q, want it to reject the empty title", err)
|
|
}
|
|
}
|
|
|
|
// execute runs the command tree with args, discarding output, so tests assert
|
|
// on the error alone. Every case here fails before any Vault/Gitea call.
|
|
func execute(args ...string) error {
|
|
cmd := newRootCmd()
|
|
cmd.SetArgs(args)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
return cmd.Execute()
|
|
}
|
|
|
|
// An issue needs a title; Gitea rejects an empty one, so the command must too.
|
|
func TestIssueCreateRequiresTitle(t *testing.T) {
|
|
err := execute("issue", "create", "--repo", "unkin/repo", "--body", "b")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error when --title is missing")
|
|
}
|
|
if !strings.Contains(err.Error(), "--title is required") {
|
|
t.Errorf("error = %q, want it to name the missing flag", err)
|
|
}
|
|
}
|
|
|
|
// --repo is required, and cobra must reject its absence before anything reaches
|
|
// for a token.
|
|
func TestIssueCreateRequiresRepo(t *testing.T) {
|
|
err := execute("issue", "create", "--title", "t")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error when --repo is missing")
|
|
}
|
|
if !strings.Contains(err.Error(), "repo") {
|
|
t.Errorf("error = %q, want it to name the missing flag", err)
|
|
}
|
|
}
|
|
|
|
func TestIssueCreateBadRepoErrors(t *testing.T) {
|
|
if err := execute("issue", "create", "--repo", "not-a-repo", "--title", "t"); err == nil {
|
|
t.Fatal("Execute() = nil, want error for a malformed --repo")
|
|
}
|
|
}
|
|
|
|
// `issue comment` addresses the issue by --issue, not --pr, and needs it.
|
|
func TestIssueCommentRequiresIssueNumber(t *testing.T) {
|
|
err := execute("issue", "comment", "--repo", "unkin/repo", "--body", "hi")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error when --issue is missing")
|
|
}
|
|
if !strings.Contains(err.Error(), "issue") {
|
|
t.Errorf("error = %q, want it to name the missing --issue flag", err)
|
|
}
|
|
}
|
|
|
|
func TestIssueEditRequiresTitleOrBody(t *testing.T) {
|
|
err := execute("issue", "edit", "--repo", "unkin/repo", "--issue", "12")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error when neither --title nor --body is given")
|
|
}
|
|
if !strings.Contains(err.Error(), "--title or --body") {
|
|
t.Errorf("error = %q, want it to name the missing flags", err)
|
|
}
|
|
}
|
|
|
|
func TestIssueEditRejectsEmptyTitle(t *testing.T) {
|
|
err := execute("issue", "edit", "--repo", "unkin/repo", "--issue", "12", "--title", "")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error for an empty --title")
|
|
}
|
|
if !strings.Contains(err.Error(), "--title cannot be empty") {
|
|
t.Errorf("error = %q, want it to reject the empty title", err)
|
|
}
|
|
}
|
|
|
|
// PRs and issues share Gitea's comment endpoint, so both comment commands are
|
|
// built from one constructor: they must stay identical apart from the flag
|
|
// naming the number.
|
|
func TestCommentCommandsStayInStep(t *testing.T) {
|
|
find := func(group string) *cobra.Command {
|
|
t.Helper()
|
|
cmd, _, err := newRootCmd().Find([]string{group, "comment"})
|
|
if err != nil || cmd.Name() != "comment" {
|
|
t.Fatalf("%s comment not found: %v", group, err)
|
|
}
|
|
return cmd
|
|
}
|
|
has := func(cmd *cobra.Command, name string) bool { return cmd.Flags().Lookup(name) != nil }
|
|
|
|
prCmd, issueCmd := find("pr"), find("issue")
|
|
for _, name := range []string{"repo", "body"} {
|
|
if !has(prCmd, name) || !has(issueCmd, name) {
|
|
t.Errorf("--%s missing: pr=%t issue=%t", name, has(prCmd, name), has(issueCmd, name))
|
|
}
|
|
}
|
|
if !has(prCmd, "pr") || has(prCmd, "issue") {
|
|
t.Error("pr comment must take --pr and only --pr")
|
|
}
|
|
if !has(issueCmd, "issue") || has(issueCmd, "pr") {
|
|
t.Error("issue comment must take --issue and only --issue")
|
|
}
|
|
}
|
|
|
|
// Both state commands need a repo and an issue number; cobra and the RunE
|
|
// guard must reject their absence before anything reaches for a token.
|
|
func TestIssueStateCommandsRequireFlags(t *testing.T) {
|
|
for _, verb := range []string{"close", "reopen"} {
|
|
t.Run(verb+" without --issue", func(t *testing.T) {
|
|
err := execute("issue", verb, "--repo", "unkin/repo")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error when --issue is missing")
|
|
}
|
|
if !strings.Contains(err.Error(), "issue") {
|
|
t.Errorf("error = %q, want it to name the missing --issue flag", err)
|
|
}
|
|
})
|
|
t.Run(verb+" without --repo", func(t *testing.T) {
|
|
err := execute("issue", verb, "--issue", "12")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error when --repo is missing")
|
|
}
|
|
if !strings.Contains(err.Error(), "repo") {
|
|
t.Errorf("error = %q, want it to name the missing --repo flag", err)
|
|
}
|
|
})
|
|
t.Run(verb+" with a zero --issue", func(t *testing.T) {
|
|
err := execute("issue", verb, "--repo", "unkin/repo", "--issue", "0")
|
|
if err == nil {
|
|
t.Fatal("Execute() = nil, want an error for a non-positive issue number")
|
|
}
|
|
if !strings.Contains(err.Error(), "--issue must be a positive") {
|
|
t.Errorf("error = %q, want it to reject the issue number", err)
|
|
}
|
|
})
|
|
t.Run(verb+" with a malformed --repo", func(t *testing.T) {
|
|
if err := execute("issue", verb, "--repo", "not-a-repo", "--issue", "12"); err == nil {
|
|
t.Fatal("Execute() = nil, want error for a malformed --repo")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Closing and reopening are issue-only: a PR is closed by a human, so the pr
|
|
// group must not grow these verbs by accident.
|
|
func TestPRHasNoStateCommands(t *testing.T) {
|
|
for _, verb := range []string{"close", "reopen"} {
|
|
if cmd, _, err := newRootCmd().Find([]string{"pr", verb}); err == nil && cmd.Name() == verb {
|
|
t.Errorf("pr %s exists; closing a PR is not agentpr's to do", verb)
|
|
}
|
|
cmd, _, err := newRootCmd().Find([]string{"issue", verb})
|
|
if err != nil || cmd.Name() != verb {
|
|
t.Fatalf("issue %s not found: %v", verb, err)
|
|
}
|
|
}
|
|
}
|