mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
tea pr review takes gh's review flags: -a/--approve, -r/--request-changes
and -c/--comment, with -b/--body, -F/--body-file and -e/--editor, and
--pending for a review to submit later. --event is hidden but still
accepted, case-insensitively, as approve, approved, request-changes,
request_changes, comment or pending. tea pr review create is the same
command under the name people look for.
The pull request argument is optional and accepts #N, URLs and branch
names; without one, the pull request of the current branch is reviewed.
Without a review type, tea opens the interactive review on a terminal
and otherwise fails with "choose a review type" instead of posting a
comment. Review types given as a subcommand ("tea pr review approve 13")
point at the flag, and approving or requesting changes on one's own pull
request fails before any editor opens.
The interactive review asks for the type first, hiding Approve and
Request changes for the author, then for an inline body that ctrl+e
opens in the configured editor, then offers to annotate the diff, and
confirms with a default of yes. A declined submission keeps the body as
a draft.
Successful reviews report "✓ Approved pull request alice/x#13 (title)"
on a terminal and print the review URL. review submit finds the
caller's pending review when no ID is given, review list and comments
are tables with headers, and dismiss, undismiss, delete, request and
cancel-request say what they did. Every review command has examples and
completes pull request and review IDs.
Signed-off-by: bircni <bircni@icloud.com>
386 lines
31 KiB
Go
386 lines
31 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dev/tea/internal/clierr"
|
|
"gitea.dev/tea/internal/terminal"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestUsageErrorsPrecedeRequests(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
}{
|
|
{[]string{"admin", "user", "create", "account", "--email", "invalid", "--password", "local-password"}, "--email: enter one email address"},
|
|
{[]string{"admin", "user", "create", "account", "--email", "account@example.test", "--password", "one", "--password-file", "-"}, "--password and --password-file are mutually exclusive"},
|
|
{[]string{"admin", "user", "create", "account", "--email", "account@example.test", "--password-file", ""}, "--password-file must name a file or -"},
|
|
{[]string{"admin", "user", "create", "account", "--email", "account@example.test", "--password", "local-password", "--visibility", "invalid"}, "--visibility must be public, limited, or private"},
|
|
{[]string{"admin", "user", "create", "account", "--username", "different"}, "supply username as an argument or --username, not both"},
|
|
{[]string{"admin", "user", "edit", "account"}, "supply account fields to edit"},
|
|
{[]string{"admin", "user", "edit", "account", "--email="}, "--email: enter one email address"},
|
|
{[]string{"admin", "user", "edit", "account", "--max-repo-creation", "-2"}, "--max-repo-creation must be -1 or nonnegative"},
|
|
{[]string{"admin", "user", "edit", "account", "--password="}, "missing password: supply --password-file or --password"},
|
|
{[]string{"admin", "user", "view", "bad/account"}, "invalid account username"},
|
|
{[]string{"api", "user", "--input", "-", "-f", "x=y"}, "--input conflicts with API fields"},
|
|
{[]string{"api", "user", "-f", "x=y", "-F", "x=2"}, "API field names must be unique"},
|
|
{[]string{"api", "user", "-f", "missing-equals"}, "API fields require name=value"},
|
|
{[]string{"api", "user", "--paginate", "-X", "POST"}, "--paginate requires a body-free GET request"},
|
|
{[]string{"api", "user", "--input="}, "--input requires a file path or -"},
|
|
{[]string{"api", "user", "-X", "invalid/method"}, "invalid HTTP method"},
|
|
{[]string{"alias", "delete", "co", "--json"}, "alias deletion does not return a resource"},
|
|
{[]string{"auth", "status", "--json=token"}, "unknown JSON field \"token\""},
|
|
{[]string{"config", "set", "prompt", "invalid"}, "prompt must be enabled or disabled"},
|
|
{[]string{"doctor", "--json=secret"}, "unknown JSON field \"secret\""},
|
|
{[]string{"issue", "attachment", "download", "1", "1"}, "supply --output-file, or --output-file - for stdout"},
|
|
{[]string{"issue", "attachment", "download", "1", "1", "--json"}, "attachment downloads are binary"},
|
|
{[]string{"issue", "attachment", "download", "1", "1", "--output-file", "-", "--json=id"}, "attachment downloads are binary"},
|
|
{[]string{"issue", "close", "1", "bad"}, "invalid resource number \"bad\""},
|
|
{[]string{"issue", "close", "1", "1", "--no-input"}, "resource 1 is selected more than once"},
|
|
{[]string{"issue", "reopen", "1", "01", "--no-input"}, "resource 1 is selected more than once"},
|
|
{[]string{"issue", "comment", "create", "1", "--body="}, "comment body cannot be empty"},
|
|
{[]string{"issue", "comment", "create", "1", "--body", "text", "--json=title"}, "unknown JSON field \"title\""},
|
|
{[]string{"issue", "create", "--no-input"}, "missing required input: supply --title"},
|
|
{[]string{"issue", "create", "--title", "safe", "--json=unknown"}, "unknown JSON field \"unknown\""},
|
|
{[]string{"issue", "create", "--title", "safe", "--json", "--jq", "["}, "invalid --jq expression"},
|
|
{[]string{"issue", "create", "--milestone=-1"}, "--milestone must be a positive ID, or 0 for none"},
|
|
{[]string{"issue", "create", "--assignee", "alice, "}, "assignee names cannot be empty"},
|
|
{[]string{"issue", "create", "--template="}, "--template cannot be empty"},
|
|
{[]string{"issue", "create", "--title", "x", "--attachment", "."}, ". is not a regular file"},
|
|
{[]string{"issue", "create", "--milestone-name="}, "--milestone-name cannot be empty"},
|
|
{[]string{"issue", "create", "--milestone-name=Release", "--milestone=1"}, "--milestone and --milestone-name are mutually exclusive"},
|
|
{[]string{"issue", "edit", "1"}, "supply an edit flag"},
|
|
{[]string{"issue", "edit", "1", "--editor=false"}, "supply an edit flag"},
|
|
{[]string{"issue", "edit", "1", "--add-labels="}, "supply an edit flag"},
|
|
{[]string{"issue", "edit", "1", "--title="}, "issue title cannot be empty"},
|
|
{[]string{"issue", "edit", "1", "--set-assignees", "alice", "--add-assignees", "bob"}, "--set-assignees cannot be combined with --add-assignees or --remove-assignees"},
|
|
{[]string{"issue", "edit", "1", "--set-labels", "bug", "--add-labels", "help"}, "--set-labels cannot be combined with --add-labels or --remove-labels"},
|
|
{[]string{"issue", "edit", "1", "--add-assignees", " "}, "assignee names cannot be empty"},
|
|
{[]string{"issue", "edit", "1", "invalid", "--title", "Updated"}, "invalid resource number \"invalid\""},
|
|
{[]string{"issue", "edit", "1", "2", "--editor"}, "--editor requires one issue"},
|
|
{[]string{"issue", "edit", "1", "01", "--title", "Updated", "--no-input"}, "resource 1 is selected more than once"},
|
|
{[]string{"issue", "list", "--limit", "0"}, `"-L, --limit" flag: must be 1..1000`},
|
|
{[]string{"issue", "list", "--page", "0"}, `"--page" flag: must be a positive integer`},
|
|
{[]string{"issue", "list", "--label", "bug, "}, "label names cannot be empty"},
|
|
{[]string{"label", "create", "--name", "bug", "--color", "wrong"}, "--color requires six hexadecimal digits"},
|
|
{[]string{"label", "edit", "1"}, "supply label fields to edit"},
|
|
{[]string{"label", "list", "--org", "team", "-R", "a/b"}, "--org and --repo are mutually exclusive"},
|
|
{[]string{"milestone", "close", "9", "09"}, "selectors resolve to the same milestone"},
|
|
{[]string{"milestone", "close", " ", "--by-name"}, "milestone name cannot be empty"},
|
|
{[]string{"milestone", "reopen", "v1", "v1", "--by-name"}, "milestone selectors must be unique"},
|
|
{[]string{"milestone", "create", "--title", "v1", "--deadline", "invalid"}, "invalid --deadline"},
|
|
{[]string{"milestone", "create", "--title", "x", "--state", "merged", "--no-input"}, "--state must be open or closed"},
|
|
{[]string{"milestone", "edit", "1"}, "supply milestone fields to edit"},
|
|
{[]string{"milestone", "edit", "9", "--title", ""}, "milestone title cannot be empty"},
|
|
{[]string{"milestone", "edit", "9", "--title", " \t "}, "milestone title cannot be empty"},
|
|
{[]string{"milestone", "issue", "add", "7", "1", "01", "--no-input"}, "resource 1 is selected more than once"},
|
|
{[]string{"milestone", "issue", "remove", "7", "1", "1", "--no-input"}, "resource 1 is selected more than once"},
|
|
{[]string{"milestone", "list", "--limit", "1001"}, `"-L, --limit" flag: must be 1..1000`},
|
|
{[]string{"milestone", "list", "--state", "merged"}, "--state must be open, closed, or all"},
|
|
{[]string{"notification", "list", "--status", "invalid"}, "--status must be unread, read, or pinned"},
|
|
{[]string{"notification", "list", "--type", "invalid"}, "--type must be issue, pull, commit, or repository"},
|
|
{[]string{"notification", "list", "--limit", "many"}, `"-L, --limit" flag: must be 1..1000`},
|
|
{[]string{"notification", "list", "--since="}, "invalid --since: use a date, RFC3339 timestamp, or duration such as 24h"},
|
|
{[]string{"notification", "list", "--since=1h", "--before=2h"}, "--since must not be later than --before"},
|
|
{[]string{"notification", "mark-read"}, "supply notification IDs or --all"},
|
|
{[]string{"notification", "mark-read", "1", "--all"}, "notification IDs and --all are mutually exclusive"},
|
|
{[]string{"notification", "mark-read", "1", "--status", "unread"}, "--status and --before require --all"},
|
|
{[]string{"notification", "mark-read", "--all", "--before="}, "invalid --before: use a date, RFC3339 timestamp, or duration such as 24h"},
|
|
{[]string{"notification", "mark-read", "9", "09"}, "resource 9 is selected more than once"},
|
|
{[]string{"notification", "watch", "--interval", "0s"}, "--interval must be positive and --timeout not negative"},
|
|
{[]string{"notification", "watch", "--timeout", "-1s"}, "--interval must be positive and --timeout not negative"},
|
|
{[]string{"notification", "watch", "--before=-1h"}, "invalid --before: relative duration cannot be negative"},
|
|
{[]string{"org", "create", "team", "--visibility", "unknown"}, "--visibility must be public, limited, or private"},
|
|
{[]string{"org", "edit", "team"}, "supply organization fields to edit"},
|
|
{[]string{"org", "list", "--all", "--username", "alice"}, "--all and --username are mutually exclusive"},
|
|
{[]string{"org", "member", "list", "team", "--limit", "0"}, `"-L, --limit" flag: must be 1..1000`},
|
|
{[]string{"pr", "attachment", "download", "1", "1"}, "supply --output-file, or --output-file - for stdout"},
|
|
{[]string{"pr", "attachment", "download", "1", "1", "--json"}, "attachment downloads are binary"},
|
|
{[]string{"pr", "attachment", "download", "1", "1", "--output-file", "-", "--json=id"}, "attachment downloads are binary"},
|
|
{[]string{"pr", "checkout", "7", "--detach", "--branch", "name"}, "--detach and --branch/--update are mutually exclusive"},
|
|
{[]string{"pr", "create", "--milestone=-1"}, "--milestone must be a positive ID, or 0 for none"},
|
|
{[]string{"pr", "create", "--assignee", "alice, "}, "assignee names cannot be empty"},
|
|
{[]string{"pr", "create", "--label", "bug, "}, "label names cannot be empty"},
|
|
{[]string{"pr", "create", "--milestone-name="}, "--milestone-name cannot be empty"},
|
|
{[]string{"pr", "create", "--milestone-name=Release", "--milestone=1"}, "--milestone and --milestone-name are mutually exclusive"},
|
|
{[]string{"pr", "create", "--agit", "--json", "--title", "x", "--no-input"}, "--json/--jq cannot be combined with --agit"},
|
|
{[]string{"pr", "create", "--topic", "x", "--title", "x", "--no-input"}, "--topic requires --agit"},
|
|
{[]string{"pr", "create", "--reviewer", "bob, "}, "reviewer names cannot be empty"},
|
|
{[]string{"pr", "create", "--reviewer-team", " "}, "team names cannot be empty"},
|
|
{[]string{"pr", "create", "--reviewer", "bob,Bob"}, `reviewer "Bob" is duplicated`},
|
|
{[]string{"pr", "create", "--agit", "--remote", "origin", "--reviewer", "bob"}, "--reviewer cannot be combined with --agit"},
|
|
{[]string{"pr", "create", "--agit", "--remote", "origin", "--reviewer-team", "core"}, "--reviewer-team cannot be combined with --agit"},
|
|
{[]string{"pr", "create", "--agit", "--remote", "origin", "--attachment", "log.txt"}, "--attachment cannot be combined with --agit"},
|
|
{[]string{"pr", "create", "--attachment", "."}, ". is not a regular file"},
|
|
{[]string{"pr", "edit", "7"}, "supply an edit flag"},
|
|
{[]string{"pr", "edit", "7", "--remove-assignees="}, "supply an edit flag"},
|
|
{[]string{"pr", "edit", "7", "--draft", "--ready"}, "--draft and --ready are mutually exclusive"},
|
|
{[]string{"pr", "edit", "7", "--title="}, "pull request title cannot be empty"},
|
|
{[]string{"pr", "edit", "7", "--base="}, "--base cannot be empty"},
|
|
{[]string{"pr", "edit", "7", "--add-reviewers=alice", "--remove-reviewers=Alice"}, "reviewer \"Alice\" is duplicated or present in both add and remove"},
|
|
{[]string{"pr", "edit", "7", "--add-reviewer-teams="}, "supply at least one reviewer or team"},
|
|
{[]string{"pr", "list", "--sort", "unknown"}, "--sort must be oldest, recentupdate, recentclose, leastupdate, mostcomment, leastcomment, or priority"},
|
|
{[]string{"pr", "list", "--milestone", "-1"}, "--milestone must be a nonnegative ID"},
|
|
{[]string{"pr", "list", "--state", "unknown"}, "--state must be open, closed, or all"},
|
|
{[]string{"pr", "list", "--limit", "0"}, `"-L, --limit" flag: must be 1..1000`},
|
|
{[]string{"pr", "review", "1", "--no-input"}, "choose a review type: --approve, --request-changes, or --comment"},
|
|
{[]string{"pr", "review", "1", "--comment", "--body="}, "a comment needs a body; pass --body, --body-file or --editor"},
|
|
{[]string{"pr", "review", "1", "--request-changes", "--no-input"}, "a change request needs a body"},
|
|
{[]string{"pr", "review", "1", "--event=unknown"}, "--event must be approve, approved, request-changes, request_changes, comment, or pending"},
|
|
{[]string{"pr", "review", "request", "1", "--team", " "}, "team names cannot be empty"},
|
|
{[]string{"pr", "review", "1", "--comments-file=-"}, "--comments-file requires a file"},
|
|
{[]string{"pr", "review", "submit", "1", "2", "--event=PENDING"}, "--event must be approve, approved, request-changes, request_changes, or comment"},
|
|
{[]string{"pr", "review", "submit", "1", "2", "--event=REQUEST_CHANGES", "--body="}, "a change request needs a body"},
|
|
{[]string{"pr", "review", "dismiss", "1", "2", "--body="}, "a dismissal needs a reason"},
|
|
{[]string{"pr", "review", "dismiss", "1", "2", "--no-input", "--yes"}, "missing required input: supply --body"},
|
|
{[]string{"pr", "review", "delete", "1", "2", "--json"}, "review deletion does not return a resource"},
|
|
{[]string{"pr", "status", "--limit=101"}, `"--limit" flag: must be 1..100`},
|
|
{[]string{"pr", "status", "--json=title"}, "unknown JSON field \"title\""},
|
|
{[]string{"release", "asset", "download", "v1", "1", "--json"}, "asset downloads are files"},
|
|
{[]string{"release", "asset", "download", "v1", "1", "--archive", "zip"}, "--archive downloads the source code only"},
|
|
{[]string{"release", "create", "v1", "--tag", "v2"}, "supply a tag argument or --tag, not both"},
|
|
{[]string{"release", "create", "--body-file", "-"}, "--notes-file - requires a tag"},
|
|
{[]string{"release", "edit", "v1"}, "supply release fields to edit"},
|
|
{[]string{"release", "edit", "v1", "v2", "--tag", "v3"}, "--tag requires one release and a nonempty new tag"},
|
|
{[]string{"repo", "create"}, "missing required input: supply --name"},
|
|
{[]string{"repo", "create", "acme/bad/name"}, "repository name must be nonempty, at most 100 bytes, and contain no path separators or whitespace"},
|
|
{[]string{"repo", "create", "name", "--name", "other"}, "supply name as an argument or --name, not both"},
|
|
{[]string{"repo", "create", "name", "--default-branch", "bad..branch", "--auto-init"}, "invalid branch name"},
|
|
{[]string{"repo", "create", "name", "--license", "MIT"}, "initial content and default branch options require --auto-init"},
|
|
{[]string{"repo", "create", "name", "--trust-model", "unknown"}, "--trust-model must be default, collaborator, committer, or collaboratorcommitter"},
|
|
{[]string{"repo", "create", "name", "--object-format", "unknown"}, "--object-format must be sha1 or sha256"},
|
|
{[]string{"repo", "create", "name", "--team", "developers"}, "--team requires --org and a team name"},
|
|
{[]string{"repo", "create", "name", "--org", "acme", "--team="}, "--team requires --org and a team name"},
|
|
{[]string{"repo", "create", "name", "--copy-content=false"}, "copy options require --template owner/repo"},
|
|
{[]string{"repo", "create", "name", "--template="}, "--template requires owner/repo or a repository URL"},
|
|
{[]string{"repo", "create", "name", "--template", "owner/source", "--auto-init"}, "repository initialization options cannot be combined with --template"},
|
|
{[]string{"repo", "create", "name", "--template", "owner/source", "-R", "owner/other"}, "--repo is not used by repo create; did you mean: tea repo create owner/name"},
|
|
{[]string{"repo", "edit", "--name="}, "--name cannot be empty"},
|
|
{[]string{"repo", "edit", "--default-branch="}, "--default-branch cannot be empty"},
|
|
{[]string{"repo", "archive", "alice/project", "-R", "alice/other", "--yes"}, "supply the repository as an argument or --repo, not both"},
|
|
{[]string{"repo", "view", "alice/project", "--repo", "alice/other"}, "supply the repository as an argument or --repo, not both"},
|
|
{[]string{"repo", "view", "alice/project", "alice/other"}, "too many arguments\nUsage: tea repo view [<repository>] [flags]"},
|
|
{[]string{"repo", "list", "--owner", "account", "--starred"}, "an owner, --starred and --watched select different collections; pass one"},
|
|
{[]string{"repo", "list", "--starred", "--watched"}, "an owner, --starred and --watched select different collections; pass one"},
|
|
{[]string{"repo", "list", "--type", "invalid"}, "--type must be fork, source, or mirror"},
|
|
{[]string{"repo", "set-default", "--json"}, "set-default configures Git"},
|
|
{[]string{"repo", "sync", "--branch="}, "--branch cannot be empty"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--clone-url", "https://other.test/repo"}, "supply clone URL as an argument or --clone-url, not both"},
|
|
{[]string{"repo", "migrate", "https://user:secret@source.test/repo", "--name", "new"}, "clone URL must be an HTTP(S) or git URL without embedded credentials"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--service", "unknown"}, "--service must be git, gitea, gitlab, gogs, or github"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--issues"}, "plain Git migration cannot copy forge metadata"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--mirror-interval", "8h"}, "--mirror-interval requires --mirror and a positive duration"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--mirror", "--issues"}, "mirrors cannot copy issues, milestones, labels, pull requests, or releases"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--lfs-endpoint", "https://lfs.test"}, "--lfs-endpoint requires --lfs"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--auth-password", "secret"}, "--auth-password requires --auth-user"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--auth-token", "one", "--auth-token-file", "-"}, "--auth-token and --auth-token-file are mutually exclusive"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--auth-password-file", "-", "--auth-token-file", "-"}, "stdin can supply only one source credential"},
|
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--timeout", "0s"}, "--timeout must be positive"},
|
|
{[]string{"repo", "migrate", "--server-path", "relative/path", "--name", "new"}, "--server-path must be an absolute path on the Gitea server"},
|
|
{[]string{"repo", "migrate", "--server-path", "/server/repo", "--name", "new", "--auth-user", "account"}, "server path imports use --service git without remote credentials"},
|
|
{[]string{"run", "list", "--since=-1h"}, "invalid --since: relative duration cannot be negative"},
|
|
{[]string{"run", "list", "--until="}, "invalid --until: use a date, RFC3339 timestamp, or duration such as 24h"},
|
|
{[]string{"run", "list", "--since=1h", "--until=2h"}, "--since must not be later than --until"},
|
|
{[]string{"run", "list", "--limit", "0"}, `"-L, --limit" flag: must be 1..1000`},
|
|
{[]string{"search", "issue", "query", "--state", "invalid"}, "--state must be open, closed, or all"},
|
|
{[]string{"search", "issue", "query", "--author", "alice", "--authored"}, "--author and --authored are mutually exclusive"},
|
|
{[]string{"search", "issue", "--label", ","}, "label names cannot be empty"},
|
|
{[]string{"search", "pr", "query", "--since", "invalid"}, "--since: use a date, RFC3339 timestamp, or duration such as 24h"},
|
|
{[]string{"search", "pr", "query", "--since", "2026-01-02T00:00:00Z", "--before", "2026-01-01T00:00:00Z"}, "--since must not be later than --before"},
|
|
{[]string{"search", "issue", "--since="}, "invalid --since: use a date"},
|
|
{[]string{"search", "repo", "query", "--sort", "invalid"}, "--sort must be alpha, created, updated, size, or id"},
|
|
{[]string{"search", "repo", "query", "--order", "invalid"}, "--order must be asc or desc"},
|
|
{[]string{"secret", "set", "BUILD_TOKEN", "--value", "text", "--value-file", "-"}, "--body and --body-file are mutually exclusive"},
|
|
{[]string{"secret", "set", "BUILD_TOKEN", "--value= ", "--json=value"}, "unknown JSON field \"value\""},
|
|
{[]string{"secret", "set", "BUILD_TOKEN", "--no-input"}, "missing value: pass --body, --body-file or standard input"},
|
|
{[]string{"secret", "set", "CI", "--value", "text"}, "CI and names starting with GITEA_ or GITHUB_ are reserved"},
|
|
{[]string{"status", "--limit=0"}, `"--limit" flag: must be 1..100`},
|
|
{[]string{"status", "--json=token"}, "unknown JSON field \"token\""},
|
|
{[]string{"team", "create", "org", "--name", "Developers", "--permission", "owner"}, "--permission must be read, write, or admin"},
|
|
{[]string{"team", "create", "org", "--name", "Developers", "--unit", "repo.unknown=write"}, `unknown unit "repo.unknown"; use one of repo.code`},
|
|
{[]string{"team", "create", "org", "--name", "Developers", "--unit", "repo.ext_wiki=write"}, "--unit repo.ext_wiki=write: access must be none, read"},
|
|
{[]string{"team", "create", "org/Developers", "--name", "Developers"}, "supply the team name once"},
|
|
{[]string{"team", "create", "org", "--name", "Developers", "--unit", "repo.code=write", "--permission", "admin"}, "--unit cannot restrict administrator access"},
|
|
{[]string{"team", "edit", "1"}, "supply team fields to edit"},
|
|
{[]string{"team", "edit", "1", "--unit="}, "must be formatted as key=value"},
|
|
{[]string{"team", "member", "add", "invalid", "alice"}, `invalid team "invalid": use ORG/TEAM, such as acme/developers, or a team ID`},
|
|
{[]string{"team", "view", "acme/"}, `invalid team "acme/"`},
|
|
{[]string{"team", "repo", "add", "2", "service", "-R", "acme/service"}, "--repo is not used by tea team repo add"},
|
|
{[]string{"time", "create", "1", "0s"}, "duration must be nonzero whole seconds, for example 1h25m"},
|
|
{[]string{"time", "create", "1", "1ms"}, "duration must be nonzero whole seconds, for example 1h25m"},
|
|
{[]string{"time", "list", "--issue", "0"}, "--issue must be positive"},
|
|
{[]string{"time", "list", "--mine", "--issue", "1"}, "--mine, --issue and --username select mutually exclusive scopes"},
|
|
{[]string{"time", "list", "--from", "2026-10-02", "--until", "2026-10-01"}, "--from must not be later than --until"},
|
|
{[]string{"time", "list", "--from", "1h", "--until", "2h"}, "--from must not be later than --until"},
|
|
{[]string{"time", "list", "--total", "--json"}, "--total is a human-output summary"},
|
|
{[]string{"variable", "set", "BUILD_MODE", "--value="}, "the value of BUILD_MODE is empty"},
|
|
{[]string{"variable", "set", "GITEA_RESERVED", "--value", "text"}, "CI and names starting with GITEA_ or GITHUB_ are reserved"},
|
|
{[]string{"variable", "list", "--org", "team", "--account"}, "--org, --account and --repo select different scopes; pass one"},
|
|
{[]string{"variable", "list", "--org", "team", "--repo", "alice/project"}, "--org, --account and --repo select different scopes; pass one"},
|
|
{[]string{"version", "--json"}, "version prints text"},
|
|
{[]string{"webhook", "edit", "1", "--secret", "private", "--clear-authorization"}, "webhook secret editing is unsupported"},
|
|
{[]string{"webhook", "create", "ftp://host/path"}, "webhook URL must be an absolute HTTP(S) URL without user information or fragment"},
|
|
{[]string{"webhook", "create", "https://user:password@host/path"}, "webhook URL must be an absolute HTTP(S) URL without user information or fragment"},
|
|
{[]string{"webhook", "create", "https://host/path#fragment"}, "webhook URL must be an absolute HTTP(S) URL without user information or fragment"},
|
|
{[]string{"webhook", "create", "https://host/path", "--url", "https://other/path"}, "supply the URL as an argument or --url, not both"},
|
|
{[]string{"webhook", "create", "https://host/path", "--secret", "one", "--secret-file", "-"}, "--secret and --secret-file are mutually exclusive"},
|
|
{[]string{"webhook", "create", "https://host/path", "--secret-file", "-", "--authorization-header-file", "-"}, "stdin can supply only one webhook credential"},
|
|
{[]string{"webhook", "create", "https://host/path", "--authorization-header", "one", "--clear-authorization"}, "choose one authorization header source or --clear-authorization"},
|
|
{[]string{"webhook", "create", "https://host/path", "--authorization-header", "first\nsecond"}, "authorization header must be a single line"},
|
|
{[]string{"webhook", "create", "https://host/path", "--events", ""}, "--events needs at least one trigger"},
|
|
{[]string{"webhook", "create", "https://host/path", "--config", "secret=private"}, "--config cannot override the URL, secret, or webhook scope"},
|
|
{[]string{"webhook", "edit", "1", "--active=false"}, "editing a webhook drops its stored authorization header unless it is sent again, and Gitea never returns it: pass --authorization-header-file or --authorization-header (use --clear-authorization only for a hook without one)"},
|
|
{[]string{"webhook", "delete", "not-an-id"}, "invalid resource number \"not-an-id\""},
|
|
{[]string{"webhook", "list", "--org", "organization", "--account"}, "--org, --account, --global, and --repo select mutually exclusive scopes"},
|
|
{[]string{"webhook", "list", "--global", "-R", "owner/repo"}, "--org, --account, --global, and --repo select mutually exclusive scopes"},
|
|
{[]string{"wiki", "create", "--body-file", "-"}, "--body-file - requires a title"},
|
|
{[]string{"wiki", "create", "--title", "Home", "--body", "text", "--json=created_at"}, "unknown JSON field \"created_at\""},
|
|
{[]string{"wiki", "edit", "Home", "--body="}, "wiki content cannot be empty"},
|
|
{[]string{"wiki", "revisions", "Home", "--page", "0"}, `"--page" flag: must be a positive integer`},
|
|
{[]string{"workflow", "run", "ci.yml", "-r", "main", "-f", "a=1", "-i", "a=2"}, "input a is given more than once"},
|
|
} {
|
|
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
|
requireUsageBeforeRequests(t, tc.want, tc.args...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfirmationsNameResolvedValues(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
args []string
|
|
inputs map[string]string
|
|
want string
|
|
}{
|
|
{[]string{"release", "create"}, map[string]string{"Tag": "v1.0.0"}, "Create release v1.0.0 in alice/project?"},
|
|
{[]string{"time", "delete", "5", "7"}, nil, "delete tracked time entry 7 on alice/project #5"},
|
|
} {
|
|
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
w.WriteHeader(http.StatusTeapot)
|
|
}))
|
|
defer server.Close()
|
|
deps, _, _ := testDeps(t)
|
|
deps.Getenv = serverEnv(server.URL, map[string]string{"TEA_REPO": "alice/project"})
|
|
prompt := &scriptedPrompter{inputs: tc.inputs}
|
|
deps.Prompter = prompt
|
|
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
|
|
err := Execute(context.Background(), deps, tc.args)
|
|
require.Equal(t, clierr.Canceled, clierr.ExitCode(err), "%v", err)
|
|
require.Len(t, prompt.confirmations, 1)
|
|
require.Contains(t, prompt.confirmations[0], tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPromptedCreateSubmitsWithoutConfirmation(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
args []string
|
|
inputs map[string]string
|
|
secret, write string
|
|
}{
|
|
{[]string{"secret", "set", "TOKEN"}, nil, "private", "PUT /api/v1/repos/alice/project/actions/secrets/TOKEN"},
|
|
{[]string{"variable", "set", "MODE"}, map[string]string{"Value of MODE:": "fast"}, "", "POST /api/v1/repos/alice/project/actions/variables/MODE"},
|
|
{[]string{"webhook", "create"}, map[string]string{"Payload URL": "https://hooks.test/path"}, "", "POST /api/v1/repos/alice/project/hooks"},
|
|
{[]string{"team", "create", "acme"}, map[string]string{"Team name": "Developers"}, "", "POST /api/v1/orgs/acme/teams"},
|
|
} {
|
|
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
|
var writes []string
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
writes = append(writes, r.Method+" "+r.URL.Path)
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = w.Write([]byte(`{"id":1}`))
|
|
}))
|
|
defer server.Close()
|
|
deps, _, _ := testDeps(t)
|
|
deps.Getenv = serverEnv(server.URL, map[string]string{"TEA_REPO": "alice/project"})
|
|
prompt := &scriptedPrompter{inputs: tc.inputs, secret: tc.secret}
|
|
deps.Prompter = prompt
|
|
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
|
|
require.NoError(t, Execute(context.Background(), deps, tc.args))
|
|
require.Empty(t, prompt.confirmations)
|
|
require.Equal(t, []string{tc.write}, writes)
|
|
})
|
|
}
|
|
}
|
|
|
|
type unexpectedForms struct{ t *testing.T }
|
|
|
|
func (f unexpectedForms) CreateIssue(context.Context, terminal.IssueFormChoices) (terminal.IssueFormValues, error) {
|
|
f.t.Error("unexpected issue form")
|
|
return terminal.IssueFormValues{}, nil
|
|
}
|
|
|
|
func (f unexpectedForms) EditIssue(context.Context, terminal.IssueEditFormChoices) (terminal.IssueEditFormValues, error) {
|
|
f.t.Error("unexpected issue edit form")
|
|
return terminal.IssueEditFormValues{}, nil
|
|
}
|
|
|
|
func (f unexpectedForms) CreatePull(context.Context, terminal.PullFormChoices) (terminal.PullFormValues, error) {
|
|
f.t.Error("unexpected pull request form")
|
|
return terminal.PullFormValues{}, nil
|
|
}
|
|
|
|
func (f unexpectedForms) CreateReview(context.Context, terminal.ReviewFormChoices) (terminal.ReviewFormValues, error) {
|
|
f.t.Error("unexpected review form")
|
|
return terminal.ReviewFormValues{}, nil
|
|
}
|
|
|
|
func TestExplicitValuesSkipInteractiveForms(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
args []string
|
|
method, path, field string
|
|
}{
|
|
{[]string{"issue", "create", "--title", "Explicit"}, http.MethodPost, "/api/v1/repos/alice/project/issues", "number"},
|
|
{[]string{"issue", "edit", "1", "--title", "Explicit"}, http.MethodPatch, "/api/v1/repos/alice/project/issues/1", "number"},
|
|
{[]string{"pr", "create", "--title", "Explicit", "--head", "feature", "--base", "main"}, http.MethodPost, "/api/v1/repos/alice/project/pulls", "number"},
|
|
{[]string{"pr", "review", "1", "--comment", "--body", "Explicit"}, http.MethodPost, "/api/v1/repos/alice/project/pulls/1/reviews", "id"},
|
|
} {
|
|
t.Run(strings.Join(tc.args[:2], " "), func(t *testing.T) {
|
|
mutations := 0
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/alice/project":
|
|
_, _ = w.Write([]byte(`{"full_name":"alice/project","default_branch":"main"}`))
|
|
return
|
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/alice/project/pulls/1":
|
|
_, _ = w.Write([]byte(`{"number":1,"user":{"login":"bob"}}`))
|
|
return
|
|
}
|
|
require.Equal(t, tc.method, r.Method)
|
|
require.Equal(t, tc.path, r.URL.Path)
|
|
mutations++
|
|
_, _ = fmt.Fprintf(w, `{%q:1}`, tc.field)
|
|
}))
|
|
defer server.Close()
|
|
deps, out, _ := serverDeps(t, server.URL)
|
|
forms := unexpectedForms{t}
|
|
deps.IssueForm, deps.IssueEditForm, deps.PullForm, deps.ReviewForm = forms, forms, forms, forms
|
|
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
|
|
deps.Git = &fakeGit{results: map[string]string{
|
|
"rev-parse --is-inside-work-tree": "true",
|
|
"rev-parse --verify refs/heads/feature^{commit}": "localsha",
|
|
"for-each-ref --format=%(objectname) refs/remotes/origin/feature": "localsha",
|
|
"remote": "origin",
|
|
"remote get-url origin": server.URL + "/alice/project.git",
|
|
"config --get branch.feature.remote": "origin",
|
|
}}
|
|
require.NoError(t, Execute(context.Background(), deps, append(tc.args, "-R", "alice/project", "--json="+tc.field, "--interactive")))
|
|
require.Equal(t, 1, mutations)
|
|
require.JSONEq(t, fmt.Sprintf(`{%q:1}`, tc.field), out.String())
|
|
})
|
|
}
|
|
}
|