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") } }