Files

332 lines
10 KiB
Go

package cli
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.knownelement.com/ukrrs/mopac-gitea-go/internal/fakegitea"
)
const fakeToken = "fake-gitea-token-0123456789"
// run boots a fake Gitea, points the CLI at it via env, and runs one
// command; it returns the exit code plus captured output.
func run(t *testing.T, srv *fakegitea.Server, args ...string) (int, string, string) {
t.Helper()
t.Setenv("GITEA_URL", srv.URL)
t.Setenv("GITEA_KEY", fakeToken)
var out, errb bytes.Buffer
code := Run(args, &out, &errb)
return code, out.String(), errb.String()
}
func newServer(t *testing.T) *fakegitea.Server {
t.Helper()
srv := fakegitea.New(fakeToken, "ukrrs")
t.Cleanup(srv.Close)
return srv
}
func TestHelpAndUsage(t *testing.T) {
srv := newServer(t)
tests := []struct {
name string
args []string
code int
}{
{name: "help", args: []string{"help"}, code: 0},
{name: "no args", args: nil, code: 1},
{name: "unknown command", args: []string{"frobnicate"}, code: 1},
{name: "unknown subcommand", args: []string{"repo", "frob"}, code: 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
code, _, errOut := run(t, srv, tt.args...)
if code != tt.code {
t.Fatalf("code = %d, want %d (stderr: %s)", code, tt.code, errOut)
}
})
}
code, out, _ := run(t, srv, "help")
if code != 0 || !strings.Contains(out, "mgit repo create") || !strings.Contains(out, "GITEA_URL") {
t.Fatalf("help output = %q", out)
}
}
func TestRepoLifecycle(t *testing.T) {
srv := newServer(t)
code, out, errOut := run(t, srv, "repo", "create", "-n", "vertical-x", "--desc", "one vertical",
"--private", "--auto-init")
if code != 0 {
t.Fatalf("repo create: code=%d stderr=%s", code, errOut)
}
if !strings.Contains(out, "ukrrs/vertical-x") || !strings.Contains(out, "private") {
t.Fatalf("out = %q", out)
}
code, out, errOut = run(t, srv, "repo", "create", "-n", "vertical-y", "-o", "json")
if code != 0 {
t.Fatalf("repo create json: %s", errOut)
}
var repo struct {
FullName string `json:"full_name"`
Private bool `json:"private"`
}
if err := json.Unmarshal([]byte(out), &repo); err != nil {
t.Fatalf("json out: %v (%s)", err, out)
}
if repo.FullName != "ukrrs/vertical-y" || repo.Private {
t.Fatalf("json = %+v", repo)
}
code, out, errOut = run(t, srv, "repo", "list")
if code != 0 {
t.Fatalf("repo list: %s", errOut)
}
if !strings.Contains(out, "ukrrs/vertical-x") || !strings.Contains(out, "ukrrs/vertical-y") {
t.Fatalf("list = %q", out)
}
code, out, errOut = run(t, srv, "repo", "list", "--owner", "other")
if code != 0 {
t.Fatalf("repo list other: %s", errOut)
}
if strings.Contains(out, "ukrrs/") {
t.Fatalf("list other = %q", out)
}
code, out, errOut = run(t, srv, "repo", "show", "ukrrs/vertical-x")
if code != 0 || !strings.Contains(out, "one vertical") {
t.Fatalf("repo show: code=%d out=%q err=%s", code, out, errOut)
}
// Duplicate create -> exit 2, one-line stderr with the http code.
code, out, errOut = run(t, srv, "repo", "create", "-n", "vertical-x")
if code != 2 {
t.Fatalf("dup create code = %d, want 2", code)
}
if !strings.Contains(errOut, "http 409") || strings.Count(strings.TrimRight(errOut, "\n"), "\n") != 0 {
t.Fatalf("dup stderr = %q", errOut)
}
}
func TestRepoCreateMissingName(t *testing.T) {
srv := newServer(t)
code, _, errOut := run(t, srv, "repo", "create")
if code != 1 || !strings.Contains(errOut, "-n") {
t.Fatalf("code=%d err=%q", code, errOut)
}
}
func TestBranchList(t *testing.T) {
srv := newServer(t)
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
code, out, errOut := run(t, srv, "branch", "list", "ukrrs/MOPAC")
if code != 0 {
t.Fatalf("branch list: %s", errOut)
}
if !strings.Contains(out, "main") || !strings.Contains(out, "feat/quota") {
t.Fatalf("out = %q", out)
}
code, out, errOut = run(t, srv, "branch", "list", "ukrrs/MOPAC", "-o", "json")
if code != 0 {
t.Fatalf("branch list json: %s", errOut)
}
var branches []struct {
Name string `json:"name"`
}
if err := json.Unmarshal([]byte(out), &branches); err != nil || len(branches) != 2 {
t.Fatalf("json = %s (%v)", out, err)
}
if code, _, errOut = run(t, srv, "branch", "list", "ukrrs/absent"); code != 2 || !strings.Contains(errOut, "http 404") {
t.Fatalf("missing repo: code=%d err=%q", code, errOut)
}
}
func TestStatusCommands(t *testing.T) {
srv := newServer(t)
srv.AddRepo("ukrrs", "MOPAC", "main", "main")
sha := srv.AddBranch("ukrrs", "MOPAC", "feat/quota")
code, out, errOut := run(t, srv, "status", "create", "ukrrs/MOPAC", sha,
"--state", "success", "--context", "ci/lint", "--description", "clean")
if code != 0 {
t.Fatalf("status create: %s", errOut)
}
if !strings.Contains(out, "success") || !strings.Contains(out, "ci/lint") {
t.Fatalf("out = %q", out)
}
// Invalid state is a usage error before any request.
if code, _, errOut = run(t, srv, "status", "create", "ukrrs/MOPAC", sha, "--state", "green"); code != 1 {
t.Fatalf("invalid state code = %d err=%q", code, errOut)
}
code, out, errOut = run(t, srv, "status", "list", "ukrrs/MOPAC", "feat/quota")
if code != 0 {
t.Fatalf("status list: %s", errOut)
}
if !strings.Contains(out, "success") || !strings.Contains(out, "ci/lint") {
t.Fatalf("out = %q", out)
}
}
func TestPullRequestFlow(t *testing.T) {
srv := newServer(t)
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
bodyFile := filepath.Join(t.TempDir(), "body.md")
os.WriteFile(bodyFile, []byte("## Scope\n- quota accounting"), 0o600)
code, out, errOut := run(t, srv, "pr", "create", "ukrrs/MOPAC",
"--title", "Quota accounting", "--body", bodyFile,
"--base", "main", "--head", "feat/quota", "-o", "json")
if code != 0 {
t.Fatalf("pr create: %s", errOut)
}
var pr struct {
Number int64 `json:"number"`
State string `json:"state"`
}
if err := json.Unmarshal([]byte(out), &pr); err != nil || pr.Number != 1 || pr.State != "open" {
t.Fatalf("json = %s (%v)", out, err)
}
code, out, errOut = run(t, srv, "pr", "list", "ukrrs/MOPAC")
if code != 0 || !strings.Contains(out, "Quota accounting") || !strings.Contains(out, "#1") {
t.Fatalf("pr list: code=%d out=%q err=%s", code, out, errOut)
}
code, out, errOut = run(t, srv, "pr", "show", "ukrrs/MOPAC", "1")
if code != 0 || !strings.Contains(out, "quota accounting") || !strings.Contains(out, "main..feat/quota") {
t.Fatalf("pr show: code=%d out=%q err=%s", code, out, errOut)
}
code, out, errOut = run(t, srv, "pr", "merge", "ukrrs/MOPAC", "1", "--do", "squash")
if code != 0 || !strings.Contains(out, "#1") || !strings.Contains(out, "squash") {
t.Fatalf("pr merge: code=%d out=%q err=%s", code, out, errOut)
}
// Merged: the open list is empty and re-merge fails with 409.
code, out, _ = run(t, srv, "pr", "list", "ukrrs/MOPAC")
if code != 0 || strings.Contains(out, "Quota accounting") {
t.Fatalf("pr list after merge = %q", out)
}
if code, _, errOut = run(t, srv, "pr", "merge", "ukrrs/MOPAC", "1"); code != 2 || !strings.Contains(errOut, "http 409") {
t.Fatalf("re-merge: code=%d err=%q", code, errOut)
}
}
func TestPullRequestUsageErrors(t *testing.T) {
srv := newServer(t)
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
tests := []struct {
name string
args []string
}{
{"missing title", []string{"pr", "create", "ukrrs/MOPAC", "--base", "main", "--head", "feat/quota"}},
{"missing base", []string{"pr", "create", "ukrrs/MOPAC", "--title", "t", "--head", "feat/quota"}},
{"missing head", []string{"pr", "create", "ukrrs/MOPAC", "--title", "t", "--base", "main"}},
{"bad repo arg", []string{"pr", "create", "MOPAC", "--title", "t", "--base", "main", "--head", "x"}},
{"non-numeric index", []string{"pr", "merge", "ukrrs/MOPAC", "abc"}},
{"bad merge style", []string{"pr", "merge", "ukrrs/MOPAC", "1", "--do", "blend"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
code, _, errOut := run(t, srv, tt.args...)
if code != 1 {
t.Fatalf("code = %d, want usage error 1 (stderr: %s)", code, errOut)
}
})
}
}
func TestFlagsAfterPositionals(t *testing.T) {
srv := newServer(t)
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
code, _, errOut := run(t, srv, "pr", "create", "ukrrs/MOPAC", "--base", "main",
"--head", "feat/quota", "--title", "flags after positional")
if code != 0 {
t.Fatalf("pr create: %s", errOut)
}
}
func TestTokenNeverInOutput(t *testing.T) {
srv := newServer(t)
srv.AddRepo("ukrrs", "MOPAC", "main", "main")
t.Setenv("GITEA_URL", srv.URL)
t.Setenv("GITEA_KEY", fakeToken)
var out, errb bytes.Buffer
srv.Token = "rotated-" + fakeToken // force a 401 body that echoes the token
code := Run([]string{"repo", "list"}, &out, &errb)
if code != 2 {
t.Fatalf("code = %d, want 2", code)
}
if strings.Contains(out.String()+errb.String(), fakeToken) {
t.Fatalf("token leaked: out=%q err=%q", out.String(), errb.String())
}
}
func TestMissingConfigIsUsageError(t *testing.T) {
t.Setenv("GITEA_URL", "")
t.Setenv("GITEA_KEY", "")
var out, errb bytes.Buffer
code := Run([]string{"repo", "list", "--config", filepath.Join(t.TempDir(), "absent")}, &out, &errb)
if code != 1 || !strings.Contains(errb.String(), "GITEA_URL") {
t.Fatalf("code=%d err=%q", code, errb.String())
}
}
func TestLooseConfigRefused(t *testing.T) {
srv := newServer(t)
path := filepath.Join(t.TempDir(), "loose.env")
os.WriteFile(path, []byte("GITEA_URL="+srv.URL+"\nGITEA_KEY="+fakeToken+"\n"), 0o644)
t.Setenv("GITEA_URL", "")
t.Setenv("GITEA_KEY", "")
var out, errb bytes.Buffer
code := Run([]string{"repo", "list", "--config", path}, &out, &errb)
if code != 1 || !strings.Contains(errb.String(), "insecure mode") {
t.Fatalf("code=%d err=%q", code, errb.String())
}
}
func TestBodyFromStdinDash(t *testing.T) {
srv := newServer(t)
srv.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota")
t.Setenv("GITEA_URL", srv.URL)
t.Setenv("GITEA_KEY", fakeToken)
var out, errb bytes.Buffer
old := os.Stdin
r, w, _ := os.Pipe()
w.WriteString("body via stdin\n")
w.Close()
os.Stdin = r
defer func() { os.Stdin = old; time.Sleep(0) }()
code := Run([]string{"pr", "create", "ukrrs/MOPAC", "--title", "stdin body",
"--body", "-", "--base", "main", "--head", "feat/quota"}, &out, &errb)
os.Stdin = old
if code != 0 {
t.Fatalf("pr create stdin: code=%d err=%s", code, errb.String())
}
stored, ok := srv.PullRequest("ukrrs", "MOPAC", 1)
if !ok || stored.Body != "body via stdin\n" {
t.Fatalf("stored body = %q ok=%v", stored.Body, ok)
}
}