Files
gitea-tea/internal/app/validation_test.go
T
bircni eedbe9893d feat(repo): show repository columns, state and README
repo list and search repo printed bare names. They now render a table
with NAME, DESCRIPTION, INFO (public or private, fork, mirror, template,
archived, empty) and UPDATED under "Showing N of M repositories ...", an
empty-state message on stderr, and tab-separated rows with the URL when
piped. --type, --sort and --order complete and validate their values.

repo view shows the visibility and kind badges, stars, forks, open
issues and pull requests, the parent of a fork, the source of a mirror,
the default branch and the clone URLs, followed by the README of the
default branch (or --branch), rendered on a terminal. The repository
JSON gains empty, stars_count, forks_count, open_issues_count,
open_pr_count, topics and updated_at.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:51:45 +02:00

382 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"}, "missing required input: supply --body"},
{[]string{"pr", "review", "1", "--body="}, "review body cannot be empty without inline comments"},
{[]string{"pr", "review", "1", "--event=unknown"}, "--event must be APPROVED, 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 APPROVED, REQUEST_CHANGES, or COMMENT"},
{[]string{"pr", "review", "submit", "1", "2", "--event=REQUEST_CHANGES", "--body="}, "review body cannot be empty for REQUEST_CHANGES"},
{[]string{"pr", "review", "dismiss", "1", "2", "--body="}, "supply a non-empty dismissal reason with --body or --body-file"},
{[]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 binary"},
{[]string{"release", "asset", "download", "v1", "1"}, "supply --output-file, or --output-file - for stdout"},
{[]string{"release", "create", "v1", "--tag", "v2"}, "supply a tag argument or --tag, not both"},
{[]string{"release", "create", "--body-file", "-"}, "--body-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 committer, collaborator, or collaborator+committer"},
{[]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="}, "repository 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 --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{"wiki", "create"}, map[string]string{"title": "Home", "body": "text"}, "create wiki page Home on alice/project"},
{[]string{"release", "create"}, map[string]string{"tag": "v1.0.0"}, "create release v1.0.0 on 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", "--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) {
if r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/alice/project" {
_, _ = w.Write([]byte(`{"full_name":"alice/project","default_branch":"main"}`))
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())
})
}
}