revert: tea 1.0 rewrite (#1122–#1126) (#1146)

Reverts #1126, #1125, #1124, #1123 and #1122.

`go.mod` keeps the Go 1.26 line with toolchain `go1.26.9` (instead of the original `go1.26.6`) so the security fix from #1131 isn't lost.

reopens #69
reopens #141
reopens #162
reopens #172
reopens #197
reopens #232
reopens #240
reopens #246
reopens #323
reopens #325
reopens #326
reopens #328
reopens #344
reopens #351
reopens #373
reopens #403
reopens #406
reopens #417
reopens #430
reopens #457
reopens #480
reopens #482
reopens #489
reopens #490
reopens #498
reopens #500
reopens #504
reopens #510
reopens #516
reopens #523
reopens #556
reopens #560
reopens #569
reopens #571
reopens #602
reopens #632
reopens #633
reopens #643
reopens #666
reopens #670
reopens #672
reopens #677
reopens #683
reopens #697
reopens #726
reopens #737
reopens #770
reopens #779
reopens #804
reopens #817
reopens #831
reopens #833
reopens #834
reopens #908
reopens #966
reopens #1023
reopens #1029
reopens #1032
reopens #1050
reopens #1052
reopens #1069
reopens #1073
reopens #1087
reopens #1091
reopens #1094
reopens #1110
reopens #1115
reopens #1116
reopens #1118

Reviewed-on: https://gitea.com/gitea/tea/pulls/1146
Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
bircni committed 2026-10-18 20:56:33 +00:00
1 parent 33bb12729e
commit c5f674111a
604 files changed
+33711 -35545

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
teacmd "gitea.dev/tea/cmd"
)
func runAdminCommand(t *testing.T, args []string) error {
t.Helper()
adminCmd := teacmd.CmdAdmin
return adminCmd.Run(t.Context(), args)
}
func createAdminTestUser(t *testing.T, client *gitea.Client, username, password string) {
t.Helper()
mustChangePassword := false
user, _, err := client.Admin.CreateUser(t.Context(), gitea.CreateUserOption{
LoginName: username,
Username: username,
Email: username + "@example.com",
Password: password,
MustChangePassword: &mustChangePassword,
})
require.NoError(t, err)
require.Equal(t, username, user.UserName)
t.Cleanup(func() {
if _, err := client.Admin.DeleteUser(t.Context(), username); err != nil {
t.Logf("failed to delete integration test user %q: %v", username, err)
}
})
}
func TestAdminUsersCreateRequiresEmail(t *testing.T) {
login := createIntegrationLogin(t)
err := runAdminCommand(t, []string{
"admin", "users", "create",
"--username", fmt.Sprintf("create-no-email-%d", time.Now().UnixNano()),
"--password", "secret123",
"--login", login.Name,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "email")
}
func TestAdminUsersCreateAndDelete(t *testing.T) {
login := createIntegrationLogin(t)
client := login.Client()
username := fmt.Sprintf("tea-admin-create-%d", time.Now().UnixNano())
err := runAdminCommand(t, []string{
"admin", "users", "create",
"--username", username,
"--email", username + "@example.com",
"--password", "secret123",
"--admin",
"--prohibit-login",
"--visibility", "limited",
"--login", login.Name,
})
require.NoError(t, err)
createdUser, _, err := client.Users.GetUserInfo(t.Context(), username)
require.NoError(t, err)
assert.Equal(t, username, createdUser.UserName)
assert.Equal(t, username+"@example.com", createdUser.Email)
assert.True(t, createdUser.IsAdmin)
assert.True(t, createdUser.ProhibitLogin)
assert.Equal(t, gitea.VisibleTypeLimited, createdUser.Visibility)
err = runAdminCommand(t, []string{
"admin", "users", "delete", username,
"--confirm",
"--login", login.Name,
})
require.NoError(t, err)
_, _, err = client.Users.GetUserInfo(t.Context(), username)
require.Error(t, err)
}
func TestAdminUsersEdit(t *testing.T) {
login := createIntegrationLogin(t)
client := login.Client()
username := fmt.Sprintf("tea-admin-edit-%d", time.Now().UnixNano())
oldPassword := "old-secret"
newPassword := "new-secret"
createAdminTestUser(t, client, username, oldPassword)
passwordFile := filepath.Join(t.TempDir(), "password.txt")
require.NoError(t, os.WriteFile(passwordFile, []byte(newPassword+"\n"), 0o600))
err := runAdminCommand(t, []string{
"admin", "users", "edit", username,
"--email", username + "+new@example.com",
"--full-name", "Tea Integration",
"--restricted",
"--password-file", passwordFile,
"--no-must-change-password",
"--visibility", "private",
"--login", login.Name,
})
require.NoError(t, err)
updatedUser, _, err := client.Users.GetUserInfo(t.Context(), username)
require.NoError(t, err)
assert.Equal(t, username+"+new@example.com", updatedUser.Email)
assert.Equal(t, "Tea Integration", updatedUser.FullName)
assert.True(t, updatedUser.IsActive)
assert.True(t, updatedUser.Restricted)
assert.False(t, updatedUser.ProhibitLogin)
assert.Equal(t, gitea.VisibleTypePrivate, updatedUser.Visibility)
passwordClient, err := gitea.NewClient(
integrationGiteaURL,
gitea.SetBasicAuth(username, newPassword),
gitea.SetGiteaVersion(""),
)
require.NoError(t, err)
me, _, err := passwordClient.Users.GetMyUserInfo(t.Context())
require.NoError(t, err)
assert.Equal(t, username, me.UserName)
}
+162
View File
@@ -0,0 +1,162 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"encoding/json"
"fmt"
"strconv"
"testing"
"time"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/require"
)
func setupOrgRepoWithLabels(t *testing.T) (org *gitea.Organization, orgRepo *gitea.Repository, orgLabel, repoLabel *gitea.Label) {
t.Helper()
login := createIntegrationLogin(t)
client := login.Client()
orgName := fmt.Sprintf("labels-org-%d", time.Now().UnixNano()%1_000_000)
orgRepoName := fmt.Sprintf("labels-repo-%d", time.Now().UnixNano()%1_000_000)
ctx := context.Background()
// Clean up any existing test data that might interfere with the test.
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
org, _, err := client.Organizations.CreateOrg(ctx, gitea.CreateOrgOption{Name: orgName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
}
})
orgRepo, _, err = client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
}
})
orgLabelName := fmt.Sprintf("org-label-%d", time.Now().UnixNano()%1_000_000)
repoLabelName := fmt.Sprintf("repo-label-%d", time.Now().UnixNano()%1_000_000)
orgLabel, _, err = client.Organizations.CreateOrgLabel(ctx, orgName, gitea.CreateOrgLabelOption{Name: orgLabelName, Color: "ff0000"})
require.NoError(t, err)
repoLabel, _, err = client.Repositories.CreateLabel(ctx, orgName, orgRepoName, gitea.CreateLabelOption{Name: repoLabelName, Color: "00ff00"})
require.NoError(t, err)
return org, orgRepo, orgLabel, repoLabel
}
func setupRepoWithLabels(t *testing.T) (repo *gitea.Repository, repoLabel *gitea.Label) {
t.Helper()
login := createIntegrationLogin(t)
client := login.Client()
repoName := fmt.Sprintf("labels-repo-%d", time.Now().UnixNano()%1_000_000)
ctx := context.Background()
// Clean up any existing test data that might interfere with the test.
_, _ = client.Repositories.DeleteRepo(ctx, login.User, repoName)
repo, _, err := client.Repositories.CreateRepo(ctx, gitea.CreateRepoOption{Name: repoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(ctx, login.User, repoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", repoName, delErr)
}
})
repoLabelName := fmt.Sprintf("repo-label-%d", time.Now().UnixNano()%1_000_000)
repoLabel, _, err = client.Repositories.CreateLabel(ctx, login.User, repoName, gitea.CreateLabelOption{Name: repoLabelName, Color: "00ff00"})
require.NoError(t, err)
return repo, repoLabel
}
func TestRunLabelsList_OrgFlagReturnsOnlyOrgLabels(t *testing.T) {
// This test verifies that when listing labels with only the --org flag set,
// only organization labels are included in the results, and repository labels are excluded.
_, orgRepo, orgLabel, _ := setupOrgRepoWithLabels(t)
labelIDs := runTeaCommand(t, "label", "ls", "--org", orgRepo.Owner.UserName, "--output", "json")
var rows []map[string]string
require.NoError(t, json.Unmarshal([]byte(labelIDs), &rows))
require.Len(t, rows, 1)
indexInt, err := strconv.ParseInt(rows[0]["index"], 10, 64)
require.NoError(t, err)
require.Equal(t, orgLabel.ID, indexInt)
require.Equal(t, orgLabel.Name, rows[0]["name"])
}
func TestRunLabelsList_RepoReturnsNoOrgLabels(t *testing.T) {
// This test verifies that when listing labels for a repository with no organization,
// organization labels are not included in the results.
repo, repoLabel := setupRepoWithLabels(t)
labelIDs := runTeaCommand(t, "label", "ls", "--repo", repo.FullName, "--output", "json")
var rows []map[string]string
require.NoError(t, json.Unmarshal([]byte(labelIDs), &rows))
require.Len(t, rows, 1)
indexInt, err := strconv.ParseInt(rows[0]["index"], 10, 64)
require.NoError(t, err)
require.Equal(t, repoLabel.ID, indexInt)
require.Equal(t, repoLabel.Name, rows[0]["name"])
}
func TestRunLabelsList_UseRepoFlagOverOrgFlag(t *testing.T) {
// This test verifies that when both --org and --repo flags are set,
// the command prioritizes the --repo flag and lists labels for the repository, ignoring the --org flag.
repo, repoLabel := setupRepoWithLabels(t)
labelIDs := runTeaCommand(t, "label", "ls", "--repo", repo.FullName, "--org", "some-other-org", "--output", "json")
var rows []map[string]string
require.NoError(t, json.Unmarshal([]byte(labelIDs), &rows))
require.Len(t, rows, 1)
indexInt, err := strconv.ParseInt(rows[0]["index"], 10, 64)
require.NoError(t, err)
require.Equal(t, repoLabel.ID, indexInt)
require.Equal(t, repoLabel.Name, rows[0]["name"])
}
func TestRunLabelsList_OrgRepoReturnsRepoAndOrgLabels(t *testing.T) {
// This test verifies that when listing labels for a repository that belongs to an organization,
// both repository and organization labels are included in the results.
_, orgRepo, orgLabel, repoLabel := setupOrgRepoWithLabels(t)
labelIDs := runTeaCommand(t, "label", "ls", "--repo", orgRepo.FullName, "--output", "json")
var rows []map[string]string
require.NoError(t, json.Unmarshal([]byte(labelIDs), &rows))
require.Len(t, rows, 2)
require.ElementsMatch(t, []string{rows[0]["name"], rows[1]["name"]}, []string{orgLabel.Name, repoLabel.Name})
require.ElementsMatch(
t,
[]string{rows[0]["index"], rows[1]["index"]},
[]string{strconv.FormatInt(orgLabel.ID, 10), strconv.FormatInt(repoLabel.ID, 10)},
)
}
func TestRunLabelsList_OrgRepoWithExcludeReturnsOnlyRepoLabels(t *testing.T) {
// This test verifies that when listing labels for a repository that belongs to an organization with the --exclude flag,
// only repository labels are included in the results, and organization labels are excluded.
_, orgRepo, _, repoLabel := setupOrgRepoWithLabels(t)
labelIDs := runTeaCommand(t, "label", "ls", "--repo", orgRepo.FullName, "--exclude-org", "--output", "json")
var rows []map[string]string
require.NoError(t, json.Unmarshal([]byte(labelIDs), &rows))
require.Len(t, rows, 1)
indexInt, err := strconv.ParseInt(rows[0]["index"], 10, 64)
require.NoError(t, err)
require.Equal(t, repoLabel.ID, indexInt)
require.Equal(t, repoLabel.Name, rows[0]["name"])
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"fmt"
"strconv"
"strings"
"testing"
"time"
"gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestCommentsCommandLifecycle exercises `tea comments add`, `list`, `edit`,
// and `delete` end-to-end against a live Gitea instance. It creates a throwaway
// repo with one issue, then drives the comment subcommands and verifies state
// via the SDK after each step.
func TestCommentsCommandLifecycle(t *testing.T) {
login := createIntegrationLogin(t)
client := login.Client()
ctx := context.Background()
repoName := fmt.Sprintf("tea-comments-integration-%d", time.Now().UnixNano())
_, _, err := client.CreateRepo(ctx, gitea.CreateRepoOption{Name: repoName, AutoInit: true})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.DeleteRepo(ctx, login.User, repoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", repoName, delErr)
}
})
repoSlug := fmt.Sprintf("%s/%s", login.User, repoName)
issue, _, err := client.Issues.CreateIssue(ctx, login.User, repoName, gitea.CreateIssueOption{
Title: "comment test issue",
Body: "scratch issue for tea comment integration tests",
})
require.NoError(t, err)
issueIdx := strconv.FormatInt(issue.Index, 10)
// add via positional body
addOutput := runTeaCommand(t,
"comments", "add",
"--login", login.Name,
"--repo", repoSlug,
issueIdx, "first comment",
)
assert.Contains(t, addOutput, "first comment")
// add via the historical shorthand (parent command, no "add" subcommand)
runTeaCommand(t,
"comments",
"--login", login.Name,
"--repo", repoSlug,
issueIdx, "second comment",
)
// list should now show both, in tabular form with IDs
listOutput := runTeaCommand(t,
"comments", "list",
"--login", login.Name,
"--repo", repoSlug,
issueIdx,
)
assert.Contains(t, listOutput, "first comment")
assert.Contains(t, listOutput, "second comment")
// pull the real IDs from the server so edit/delete have something to act on
apiComments, _, err := client.Issues.ListIssueComments(ctx, login.User, repoName, issue.Index, gitea.ListIssueCommentOptions{})
require.NoError(t, err)
require.Len(t, apiComments, 2)
var firstID, secondID int64
for _, c := range apiComments {
switch {
case strings.Contains(c.Body, "first comment"):
firstID = c.ID
case strings.Contains(c.Body, "second comment"):
secondID = c.ID
}
}
require.NotZero(t, firstID, "first comment id not found in listing")
require.NotZero(t, secondID, "second comment id not found in listing")
// edit the first comment via positional body
editOutput := runTeaCommand(t,
"comments", "edit",
"--login", login.Name,
"--repo", repoSlug,
strconv.FormatInt(firstID, 10), "first comment (edited)",
)
assert.Contains(t, editOutput, "first comment (edited)")
edited, _, err := client.Issues.GetIssueComment(ctx, login.User, repoName, firstID)
require.NoError(t, err)
assert.Equal(t, "first comment (edited)", edited.Body)
// delete both comments in one batch
deleteOutput := runTeaCommand(t,
"comments", "delete",
"--login", login.Name,
"--repo", repoSlug,
strconv.FormatInt(firstID, 10),
strconv.FormatInt(secondID, 10),
)
assert.Contains(t, deleteOutput, fmt.Sprintf("Deleted comment %d", firstID))
assert.Contains(t, deleteOutput, fmt.Sprintf("Deleted comment %d", secondID))
remaining, _, err := client.Issues.ListIssueComments(ctx, login.User, repoName, issue.Index, gitea.ListIssueCommentOptions{})
require.NoError(t, err)
assert.Empty(t, remaining, "expected all comments to be deleted")
}
@@ -0,0 +1,80 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build unix
package integration
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"gitea.dev/tea/modules/config"
)
func TestConfigLock_CrossProcess(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "tea-lock-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
lockPath := filepath.Join(tmpDir, "config.yml.lock")
unlock, err := config.AcquireConfigLockForTesting(lockPath, 5*time.Second)
if err != nil {
t.Fatalf("failed to acquire lock: %v", err)
}
defer func() {
if err := unlock(); err != nil {
t.Fatalf("failed to release lock: %v", err)
}
}()
script := fmt.Sprintf(`
package main
import (
"os"
"syscall"
)
func main() {
file, err := os.OpenFile(%q, os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
os.Exit(2)
}
defer file.Close()
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
os.Exit(0)
}
syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
os.Exit(1)
}
`, lockPath)
scriptPath := filepath.Join(tmpDir, "locktest.go")
if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil {
t.Fatalf("failed to write test script: %v", err)
}
cmd := exec.Command("go", "run", scriptPath)
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 1 {
t.Error("subprocess acquired lock when it should have been held")
} else if exitErr.ExitCode() == 2 {
t.Errorf("subprocess failed to open lock file: %v", err)
}
} else {
t.Errorf("subprocess execution failed: %v", err)
}
}
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"gitea.dev/tea/modules/config"
teacontext "gitea.dev/tea/modules/context"
)
func TestInitCommand_WithRepoSlugKeepsLocalRepoDetection(t *testing.T) {
tmpDir := t.TempDir()
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "test-login",
URL: "https://gitea.example.com",
Token: "token",
User: "login-user",
Default: true,
}},
})
cmd := exec.Command("git", "init", "--object-format=sha256", tmpDir)
cmd.Env = os.Environ()
require.NoError(t, cmd.Run())
oldWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(tmpDir))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWd))
})
cliCmd := cli.Command{
Name: "branches",
Flags: []cli.Flag{
&cli.StringFlag{Name: "login"},
&cli.StringFlag{Name: "repo"},
&cli.StringFlag{Name: "remote"},
&cli.StringFlag{Name: "output"},
},
}
require.NoError(t, cliCmd.Set("repo", "owner/repo"))
ctx, err := teacontext.InitCommand(&cliCmd)
require.NoError(t, err)
require.Equal(t, "owner", ctx.Owner)
require.Equal(t, "repo", ctx.Repo)
require.Equal(t, "owner/repo", ctx.RepoSlug)
require.NotNil(t, ctx.LocalRepo)
require.NotNil(t, ctx.Login)
require.Equal(t, "test-login", ctx.Login.Name)
cliCmd = cli.Command{
Name: "pr",
Flags: []cli.Flag{
&cli.StringFlag{Name: "repo"},
&cli.StringFlag{Name: "base"},
},
}
require.NoError(t, cliCmd.Set("repo", "owner/repo"))
require.NoError(t, cliCmd.Set("base", "main"))
ctx, err = teacontext.InitCommand(&cliCmd)
require.NoError(t, err)
require.Equal(t, "repo", ctx.Repo)
require.Equal(t, "owner/repo", ctx.RepoSlug)
require.Equal(t, "main", ctx.String("base"))
require.NotNil(t, ctx.LocalRepo)
require.NotNil(t, ctx.Login)
require.Equal(t, "test-login", ctx.Login.Name)
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
teagit "gitea.dev/tea/modules/git"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTeaCheckoutRemoteReferenceKeepsWorktreeClean(t *testing.T) {
clonePath := setupGitCheckoutTestRepo(t)
t.Chdir(clonePath)
repo, err := teagit.RepoFromPath(clonePath)
require.NoError(t, err)
err = repo.TeaCheckout(teagit.NewRemoteReferenceName("origin", "feature/test-branch"))
require.NoError(t, err)
assert.Empty(t, gitOutput(t, clonePath, "status", "--porcelain"))
assert.Equal(t, "HEAD", gitOutput(t, clonePath, "rev-parse", "--abbrev-ref", "HEAD"))
}
func TestTeaCreateBranchTracksRemoteBranch(t *testing.T) {
clonePath := setupGitCheckoutTestRepo(t)
t.Chdir(clonePath)
repo, err := teagit.RepoFromPath(clonePath)
require.NoError(t, err)
err = repo.TeaCreateBranch("pulls/123", "feature/test-branch", "origin")
require.NoError(t, err)
err = repo.TeaCheckout(teagit.NewBranchReferenceName("pulls/123"))
require.NoError(t, err)
assert.Empty(t, gitOutput(t, clonePath, "status", "--porcelain"))
assert.Equal(t, "origin", gitOutput(t, clonePath, "config", "--get", "branch.pulls/123.remote"))
assert.Equal(t, "refs/heads/feature/test-branch", gitOutput(t, clonePath, "config", "--get", "branch.pulls/123.merge"))
assert.Equal(t, "pulls/123", gitOutput(t, clonePath, "rev-parse", "--abbrev-ref", "HEAD"))
}
func setupGitCheckoutTestRepo(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
remotePath := filepath.Join(tmpDir, "remote.git")
seedPath := filepath.Join(tmpDir, "seed")
clonePath := filepath.Join(tmpDir, "clone")
runGit(t, tmpDir, "init", "--bare", remotePath)
runGit(t, tmpDir, "init", seedPath)
runGit(t, seedPath, "config", "user.email", "test@example.com")
runGit(t, seedPath, "config", "user.name", "Test User")
require.NoError(t, os.WriteFile(filepath.Join(seedPath, "README.md"), []byte("# Test Repo\n"), 0o644))
runGit(t, seedPath, "add", "README.md")
runGit(t, seedPath, "commit", "-m", "Initial commit")
runGit(t, seedPath, "branch", "-M", "main")
runGit(t, seedPath, "remote", "add", "origin", remotePath)
runGit(t, seedPath, "push", "-u", "origin", "main")
runGit(t, seedPath, "checkout", "-b", "feature/test-branch")
require.NoError(t, os.WriteFile(filepath.Join(seedPath, "feature.txt"), []byte("feature\n"), 0o644))
runGit(t, seedPath, "add", "feature.txt")
runGit(t, seedPath, "commit", "-m", "Add feature")
runGit(t, seedPath, "push", "-u", "origin", "feature/test-branch")
runGit(t, tmpDir, "clone", remotePath, clonePath)
return clonePath
}
func runGit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git %s failed: %s", strings.Join(args, " "), strings.TrimSpace(string(output)))
}
func gitOutput(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git %s failed: %s", strings.Join(args, " "), strings.TrimSpace(string(output)))
return strings.TrimSpace(string(output))
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
teagit "gitea.dev/tea/modules/git"
)
func TestRepoFromPath_Worktree(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "tea-worktree-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
mainRepoPath := filepath.Join(tmpDir, "main-repo")
worktreePath := filepath.Join(tmpDir, "worktree")
cmd := exec.Command("git", "init", mainRepoPath)
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.email", "test@example.com")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "config", "commit.gpgsign", "false")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git")
assert.NoError(t, cmd.Run())
readmePath := filepath.Join(mainRepoPath, "README.md")
err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644)
assert.NoError(t, err)
cmd = exec.Command("git", "-C", mainRepoPath, "add", "README.md")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "commit", "-m", "Initial commit")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch")
assert.NoError(t, cmd.Run())
repo, err := teagit.RepoFromPath(worktreePath)
assert.NoError(t, err, "Should be able to open worktree")
config, err := repo.Config()
assert.NoError(t, err, "Should be able to read config")
assert.NotEmpty(t, config.Remotes, "Should be able to read remotes from worktree")
assert.Contains(t, config.Remotes, "origin", "Should have origin remote")
assert.Equal(t, "https://gitea.com/owner/repo.git", config.Remotes["origin"].URLs[0], "Should have correct remote URL")
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
stdctx "context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/require"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/task"
)
var (
integrationGiteaURL string
integrationUsername string
integrationPassword string
integrationToken string
integrationTokenID int64
integrationSetupErr error
integrationClient *gitea.Client
)
func TestMain(m *testing.M) {
integrationGiteaURL = os.Getenv("GITEA_TEA_TEST_URL")
integrationUsername = os.Getenv("GITEA_TEA_TEST_USERNAME")
integrationPassword = os.Getenv("GITEA_TEA_TEST_PASSWORD")
if integrationGiteaURL != "" {
if integrationUsername == "" || integrationPassword == "" {
integrationSetupErr = fmt.Errorf("GITEA_TEA_TEST_USERNAME and GITEA_TEA_TEST_PASSWORD are required for integration tests")
} else {
integrationClient, integrationSetupErr = gitea.NewClient(
integrationGiteaURL,
gitea.SetBasicAuth(integrationUsername, integrationPassword),
gitea.SetGiteaVersion(""),
)
if integrationSetupErr == nil {
tokenName := fmt.Sprintf("tea-integration-%d", time.Now().UnixNano())
var token *gitea.AccessToken
token, _, integrationSetupErr = integrationClient.Users.CreateAccessToken(stdctx.Background(), gitea.CreateAccessTokenOption{
Name: tokenName,
Scopes: []gitea.AccessTokenScope{gitea.AccessTokenScopeAll},
})
if integrationSetupErr == nil {
integrationToken = token.Token
integrationTokenID = token.ID
}
}
}
}
exitCode := m.Run()
if integrationClient != nil && integrationTokenID != 0 {
if _, err := integrationClient.Users.DeleteAccessToken(stdctx.Background(), integrationTokenID); err != nil {
fmt.Fprintf(os.Stderr, "failed to delete integration token %d: %v\n", integrationTokenID, err)
if exitCode == 0 {
exitCode = 1
}
}
}
os.Exit(exitCode)
}
func useTempConfigPath(t *testing.T) string {
t.Helper()
configPath := filepath.Join(t.TempDir(), "config.yml")
config.SetConfigPathForTesting(configPath)
config.SetConfigForTesting(config.LocalConfig{})
t.Cleanup(func() {
config.SetConfigForTesting(config.LocalConfig{})
config.SetConfigPathForTesting("")
})
return configPath
}
func createIntegrationLogin(t *testing.T) *config.Login {
t.Helper()
_ = useTempConfigPath(t)
if integrationGiteaURL == "" {
t.Skip("GITEA_TEA_TEST_URL is not set, skipping integration test")
}
require.NoError(t, integrationSetupErr)
require.NotEmpty(t, integrationToken, "integration token setup failed")
require.NoError(t, task.CreateLogin(t.Context(), "integration", integrationToken, "", "", "", "", "", integrationGiteaURL, "", "", true, false, false, false))
login, err := config.GetLoginByName("integration")
require.NoError(t, err)
require.NotNil(t, login)
return login
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"strings"
"testing"
"time"
"gitea.dev/tea/cmd/pulls"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestPullsReply(t *testing.T) {
login := createIntegrationLogin(t)
client := login.Client()
timestamp := time.Now().UnixNano()
repoName := fmt.Sprintf("tea-pr-reply-%d", timestamp)
featureBranch := fmt.Sprintf("reply-test-%d", timestamp)
replyBody := fmt.Sprintf("Thanks for the review %d", timestamp)
repo, _, err := client.Repositories.CreateRepo(t.Context(), gitea.CreateRepoOption{
Name: repoName,
AutoInit: true,
DefaultBranch: "main",
})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(t.Context(), login.User, repoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", repoName, delErr)
}
})
baseBranch := repo.DefaultBranch
if baseBranch == "" {
baseBranch = "main"
}
_, _, err = client.Repositories.CreateFile(t.Context(), login.User, repoName, "review.txt", gitea.CreateFileOptions{
FileOptions: gitea.FileOptions{
Message: "add review target",
BranchName: baseBranch,
NewBranchName: featureBranch,
},
Content: base64.StdEncoding.EncodeToString([]byte("line for review\n")),
})
require.NoError(t, err)
pr, _, err := client.PullRequests.CreatePullRequest(t.Context(), login.User, repoName, gitea.CreatePullRequestOption{
Base: baseBranch,
Head: featureBranch,
Title: "Integration test for pr reply",
Body: "Adds a file so we can reply to a review comment.",
})
require.NoError(t, err)
review, _, err := client.PullRequests.CreatePullReview(t.Context(), login.User, repoName, pr.Index, gitea.CreatePullReviewOptions{
State: gitea.ReviewStateComment,
Body: "Please take another look.",
Comments: []gitea.CreatePullReviewComment{{
Path: "review.txt",
Body: "Could you clarify this line?",
NewLineNum: 1,
}},
})
require.NoError(t, err)
comments, _, err := client.PullRequests.ListPullReviewComments(t.Context(), login.User, repoName, pr.Index, review.ID)
require.NoError(t, err)
require.Len(t, comments, 1)
pullsCmd := &cli.Command{
Name: "pulls",
Commands: []*cli.Command{&pulls.CmdPullsReply},
}
err = pullsCmd.Run(context.Background(), []string{
"pulls",
"reply",
strconv.FormatInt(pr.Index, 10),
strconv.FormatInt(comments[0].ID, 10),
replyBody,
"--login",
login.Name,
"--repo",
repo.FullName,
})
if err != nil && strings.Contains(err.Error(), "unknown API error: 405") {
t.Skip("pull review comment replies are not supported by this integration Gitea instance")
}
require.NoError(t, err)
require.Eventually(t, func() bool {
reviewComments, _, listErr := client.PullRequests.ListPullReviewComments(t.Context(), login.User, repoName, pr.Index, review.ID)
if listErr != nil {
t.Logf("failed to list review comments: %v", listErr)
return false
}
for _, reviewComment := range reviewComments {
if reviewComment.Body == replyBody && reviewComment.ReviewID == review.ID {
return true
}
}
return false
}, 10*time.Second, 500*time.Millisecond)
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"testing"
"time"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/repos"
)
func TestCreateRepoObjectFormat(t *testing.T) {
login := createIntegrationLogin(t)
client := login.Client()
timestamp := time.Now().Unix()
tests := []struct {
name string
args []string
wantOpts gitea.CreateRepoOption
wantErr bool
errContains string
}{
{
name: "create repo with sha1 object format",
args: []string{"--name", fmt.Sprintf("test-sha1-%d", timestamp), "--object-format", "sha1"},
wantOpts: gitea.CreateRepoOption{
Name: fmt.Sprintf("test-sha1-%d", timestamp),
ObjectFormatName: "sha1",
},
wantErr: false,
},
{
name: "create repo with sha256 object format",
args: []string{"--name", fmt.Sprintf("test-sha256-%d", timestamp), "--object-format", "sha256"},
wantOpts: gitea.CreateRepoOption{
Name: fmt.Sprintf("test-sha256-%d", timestamp),
ObjectFormatName: "sha256",
},
wantErr: false,
},
{
name: "create repo with invalid object format",
args: []string{"--name", fmt.Sprintf("test-invalid-%d", timestamp), "--object-format", "invalid"},
wantErr: true,
errContains: "invalid object format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reposCmd := &cli.Command{
Name: "repos",
Commands: []*cli.Command{&repos.CmdRepoCreate},
}
args := append([]string{"repos", "create"}, tt.args...)
args = append(args, "--login", login.Name)
err := reposCmd.Run(t.Context(), args)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(t.Context(), login.User, tt.wantOpts.Name); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", tt.wantOpts.Name, delErr)
}
})
})
}
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"strconv"
"testing"
"time"
gitea "gitea.dev/sdk"
sshkeyscmd "gitea.dev/tea/cmd/sshkeys"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"golang.org/x/crypto/ssh"
)
// generateTestPublicKey creates a fresh ed25519 keypair and returns a temp
// file path containing the public key in authorized_keys format.
func generateTestPublicKey(t *testing.T) string {
t.Helper()
_, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
sshPub, err := ssh.NewPublicKey(priv.Public())
require.NoError(t, err)
pubKeyStr := fmt.Sprintf("ssh-ed25519 %s tea-test-key", base64.StdEncoding.EncodeToString(sshPub.Marshal()))
f, err := os.CreateTemp(t.TempDir(), "test-*.pub")
require.NoError(t, err)
_, err = f.WriteString(pubKeyStr)
require.NoError(t, err)
require.NoError(t, f.Close())
return f.Name()
}
func sshKeysCmd() *cli.Command {
return &cli.Command{
Name: "ssh-keys",
Commands: []*cli.Command{
&sshkeyscmd.CmdSSHKeyList,
&sshkeyscmd.CmdSSHKeyAdd,
&sshkeyscmd.CmdSSHKeyDelete,
},
}
}
func TestSSHKeyAddAndDelete(t *testing.T) {
login := createIntegrationLogin(t)
pubKeyFile := generateTestPublicKey(t)
keyTitle := fmt.Sprintf("tea-test-%d", time.Now().Unix())
cmd := sshKeysCmd()
client := login.Client()
err := cmd.Run(t.Context(), []string{
"ssh-keys", "add", pubKeyFile,
"--title", keyTitle,
"--login", login.Name,
})
require.NoError(t, err)
keys, _, err := client.Users.ListMyPublicKeys(t.Context(), gitea.ListPublicKeysOptions{
ListOptions: gitea.ListOptions{Page: -1},
})
require.NoError(t, err)
var addedKey *gitea.PublicKey
for _, key := range keys {
if key.Title == keyTitle {
addedKey = key
break
}
}
require.NotNil(t, addedKey, "added key not found in key list")
t.Cleanup(func() {
client.Users.DeletePublicKey(t.Context(), addedKey.ID) //nolint:errcheck
})
err = cmd.Run(t.Context(), []string{
"ssh-keys", "delete", strconv.FormatInt(addedKey.ID, 10),
"--confirm",
"--login", login.Name,
})
assert.NoError(t, err)
_, resp, err := client.Users.GetPublicKey(t.Context(), addedKey.ID)
assert.Error(t, err)
if assert.NotNil(t, resp) {
assert.Equal(t, 404, resp.StatusCode)
}
}
func TestSSHKeyList(t *testing.T) {
login := createIntegrationLogin(t)
cmd := sshKeysCmd()
err := cmd.Run(t.Context(), []string{
"ssh-keys", "list",
"--login", login.Name,
})
assert.NoError(t, err)
}
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"fmt"
"testing"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd"
"github.com/stretchr/testify/require"
)
func TestEditIssue_ModifiesAssignees(t *testing.T) {
// This test verifies that EditIssue correctly modifies assignees of an issue via set, add, and remove.
// It sets up a test repository and organization with known users, then calls EditIssue and checks the results.
login := createIntegrationLogin(t)
client := login.Client()
orgName := fmt.Sprintf("issue-org-%d", time.Now().UnixNano()%1_000_000)
orgRepoName := fmt.Sprintf("issue-repo-%d", time.Now().UnixNano()%1_000_000)
ctx := context.Background()
// Clean up any existing test data that might interfere with the test.
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
_, _, err := client.Admin.CreateOrg(ctx, integrationUsername, gitea.CreateOrgOption{Name: orgName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
}
})
orgRepo, _, err := client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
}
})
user1, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user1", Password: "user1!1234", Email: "user1@test.com"})
require.NoError(t, err)
user2, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user2", Password: "user2!1234", Email: "user2@test.com"})
require.NoError(t, err)
t.Cleanup(func() {
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
})
permission := gitea.AccessModeOwner
team, _, err := client.Organizations.CreateTeam(ctx, orgName, gitea.CreateTeamOption{Name: "writers", Permission: permission})
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user1")
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user2")
require.NoError(t, err)
_, err = client.Organizations.AddTeamRepository(ctx, team.ID, orgName, orgRepoName)
require.NoError(t, err)
assigneeValid, _, err := client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user1.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
assigneeValid, _, err = client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user2.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
orgIssue, _, err := client.Issues.CreateIssue(ctx, orgName, orgRepoName, gitea.CreateIssueOption{Title: "issue_integration_test", Assignees: []string{integrationUsername}, Closed: false})
require.NoError(t, err)
require.Equal(t, integrationUsername, orgIssue.Assignees[0].UserName)
curUser, _, err := client.Users.GetMyUserInfo(ctx)
require.NoError(t, err)
getNames := func(issue *gitea.Issue) []string {
names := make([]string, len(issue.Assignees))
for i, u := range issue.Assignees {
names[i] = u.UserName
}
return names
}
checkAssignees := func(expected []string) {
updatedIssue, _, err := client.Issues.GetIssue(ctx, orgName, orgRepoName, orgIssue.Index)
require.NoError(t, err)
require.ElementsMatch(t, getNames(updatedIssue), expected)
}
app := cmd.App()
// test set overwrites add and remove
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user1", "--add-assignees", "user2", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user2," + integrationUsername, "--add-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user2.UserName, curUser.UserName})
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user1", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
// test remove one assignee
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
// test add multiple assignees and overwrites remove
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user1," + integrationUsername, "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName})
// test add one assignee
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user2", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName, user2.UserName})
// test remove multiple assignees
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user1,user2," + integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user_not_exists"})
require.Error(t, err)
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"testing"
"time"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/require"
)
func TestResolveLabelNames_ReturnsRepoAndOrgLabels(t *testing.T) {
// This test verifies that ResolveLabelNames correctly returns both repository and organization labels.
// It sets up a test repository and organization with known labels, then calls ResolveLabelNames and checks the results.
login := createIntegrationLogin(t)
client := login.Client()
orgName := fmt.Sprintf("labels-org-%d", time.Now().UnixNano()%1_000_000)
orgRepoName := fmt.Sprintf("labels-repo-%d", time.Now().UnixNano()%1_000_000)
ctx := context.Background()
// Clean up any existing test data that might interfere with the test.
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
_, _, err := client.Admin.CreateOrg(ctx, integrationUsername, gitea.CreateOrgOption{Name: orgName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
}
})
orgRepo, _, err := client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
}
})
orgLabelName := fmt.Sprintf("org-label-%d", time.Now().UnixNano()%1_000_000)
repoLabelName := fmt.Sprintf("repo-label-%d", time.Now().UnixNano()%1_000_000)
orgLabel, _, err := client.Organizations.CreateOrgLabel(ctx, orgName, gitea.CreateOrgLabelOption{Name: orgLabelName, Color: "ff0000"})
require.NoError(t, err)
repoLabel, _, err := client.Repositories.CreateLabel(ctx, orgName, orgRepoName, gitea.CreateLabelOption{Name: repoLabelName, Color: "00ff00"})
require.NoError(t, err)
tmpDir := t.TempDir()
runGit := func(args ...string) {
cmd := exec.Command("git", args...)
cmd.Dir = tmpDir
require.NoError(t, cmd.Run())
}
runGit("init")
runGit("config", "user.email", "test@test.com")
runGit("config", "user.name", "test")
httpsURL := fmt.Sprintf("%s/%s.git", login.URL, orgRepo.FullName)
httpsURL = strings.Replace(httpsURL, "://", fmt.Sprintf("://%s:%s@", login.Name, login.Token), 1)
runGit("remote", "add", "origin", httpsURL)
runGit("checkout", "-b", "main")
runGit("commit", "--allow-empty", "-m", "Initial commit")
runGit("push", "-u", "origin", "HEAD:main")
runGit("checkout", "-b", "branch-with-labels")
runGit("commit", "--allow-empty", "-m", "Initial commit")
runGit("push", "-u", "origin", "HEAD:branch-with-labels")
waitForBranches(t, orgRepo.FullName, "branch-with-labels")
_ = runTeaCommand(
t, "pr", "create", "--repo", orgRepo.FullName,
"--login", login.Name, "--base", "main", "--head", "branch-with-labels",
"--labels", orgLabelName+","+repoLabelName,
)
pr, _, err := client.PullRequests.GetPullRequest(ctx, orgName, orgRepoName, 1)
require.NoError(t, err)
labels := pr.Labels
require.Len(t, labels, 2)
require.ElementsMatch(t, labels, []*gitea.Label{orgLabel, repoLabel})
}
func waitForBranches(t *testing.T, repoFullName string, branchName string) {
t.Helper()
url := fmt.Sprintf("%s/api/v1/repos/%s/branches", os.Getenv("GITEA_TEA_TEST_URL"), repoFullName)
// retry for ~6 seconds
for range 30 {
resp, err := http.Get(url)
if err == nil {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var branches []struct {
Name string `json:"name"`
}
if json.Unmarshal(body, &branches) == nil {
have := map[string]bool{}
for _, b := range branches {
have[b.Name] = true
}
if have["main"] && have[branchName] {
return
}
}
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("waitForBranches: branches never appeared in API: %s", url)
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"fmt"
"os/exec"
"strings"
"testing"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd"
"github.com/stretchr/testify/require"
)
func TestEditPull_ModifiesAssignees(t *testing.T) {
// This test verifies that EditPull correctly modifies assignees of an pull request via set, add, and remove.
// It sets up a test repository and organization with known users, then calls EditPull and checks the results.
login := createIntegrationLogin(t)
client := login.Client()
orgName := fmt.Sprintf("pull-org-%d", time.Now().UnixNano()%1_000_000)
orgRepoName := fmt.Sprintf("pull-repo-%d", time.Now().UnixNano()%1_000_000)
ctx := context.Background()
// Clean up any existing test data that might interfere with the test.
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
_, _, err := client.Admin.CreateOrg(ctx, integrationUsername, gitea.CreateOrgOption{Name: orgName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
}
})
orgRepo, _, err := client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
}
})
user1, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user1", Password: "user1!1234", Email: "user1@test.com"})
require.NoError(t, err)
user2, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user2", Password: "user2!1234", Email: "user2@test.com"})
require.NoError(t, err)
t.Cleanup(func() {
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
})
permission := gitea.AccessModeOwner
team, _, err := client.Organizations.CreateTeam(ctx, orgName, gitea.CreateTeamOption{Name: "writers", Permission: permission})
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user1")
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user2")
require.NoError(t, err)
_, err = client.Organizations.AddTeamRepository(ctx, team.ID, orgName, orgRepoName)
require.NoError(t, err)
assigneeValid, _, err := client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user1.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
assigneeValid, _, err = client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user2.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
tmpDir := t.TempDir()
runGit := func(args ...string) {
cmd := exec.Command("git", args...)
cmd.Dir = tmpDir
require.NoError(t, cmd.Run())
}
runGit("init")
runGit("config", "user.email", "test@test.com")
runGit("config", "user.name", "test")
httpsURL := fmt.Sprintf("%s/%s.git", login.URL, orgRepo.FullName)
httpsURL = strings.Replace(httpsURL, "://", fmt.Sprintf("://%s:%s@", login.Name, login.Token), 1)
runGit("remote", "add", "origin", httpsURL)
runGit("checkout", "-b", "main")
runGit("commit", "--allow-empty", "-m", "Initial commit")
runGit("push", "-u", "origin", "HEAD:main")
runGit("checkout", "-b", "branch-with-assignees")
runGit("commit", "--allow-empty", "-m", "Initial commit")
runGit("push", "-u", "origin", "HEAD:branch-with-assignees")
waitForBranches(t, orgRepo.FullName, "branch-with-assignees")
getNames := func(pr *gitea.PullRequest) []string {
names := make([]string, len(pr.Assignees))
for i, u := range pr.Assignees {
names[i] = u.UserName
}
return names
}
checkAssignees := func(expected []string) {
updatedPr, _, err := client.PullRequests.GetPullRequest(ctx, orgName, orgRepoName, 1)
require.NoError(t, err)
require.ElementsMatch(t, getNames(updatedPr), expected)
}
app := cmd.App()
err = app.Run(ctx, []string{"tea", "pr", "create", "--repo", orgRepo.FullName, "--base", "main", "--head", "branch-with-assignees", "--a", integrationUsername})
require.NoError(t, err)
checkAssignees([]string{integrationUsername})
curUser, _, err := client.Users.GetMyUserInfo(ctx)
require.NoError(t, err)
// test set overwrites add and remove
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user1", "--add-assignees", "user2", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user2," + integrationUsername, "--add-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user2.UserName, curUser.UserName})
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user1", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
// test remove one assignee
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
// test add multiple assignees and overwrites remove
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user1," + integrationUsername, "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName})
// test add one assignee
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user2", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName, user2.UserName})
// test remove multiple assignees
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user1,user2," + integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user_not_exists"})
require.Error(t, err)
}
+170
View File
@@ -0,0 +1,170 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
gitea "gitea.dev/sdk"
teacmd "gitea.dev/tea/cmd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWikiCommandLifecycle(t *testing.T) {
login := createIntegrationLogin(t)
client := login.Client()
repoName := fmt.Sprintf("tea-wiki-integration-%d", time.Now().UnixNano())
_, _, err := client.Repositories.CreateRepo(context.Background(), gitea.CreateRepoOption{Name: repoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(context.Background(), login.User, repoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", repoName, delErr)
}
})
repoSlug := fmt.Sprintf("%s/%s", login.User, repoName)
createOutput := runTeaCommand(t,
"wiki", "create",
"--login", login.Name,
"--repo", repoSlug,
"--title", "Home",
"--content", "# Home",
"--message", "create home",
"--output", "json",
)
assert.Contains(t, createOutput, "\"title\": \"Home\"")
page, _, err := client.Wiki.GetPage(context.Background(), login.User, repoName, "Home")
require.NoError(t, err)
assert.Equal(t, "Home", page.Title)
assert.Contains(t, decodeBase64String(t, page.ContentBase64), "# Home")
listOutput := runTeaCommand(t,
"wiki", "list",
"--login", login.Name,
"--repo", repoSlug,
"--output", "json",
)
assert.Contains(t, listOutput, "\"title\": \"Home\"")
emptyRepoName := fmt.Sprintf("tea-wiki-empty-%d", time.Now().UnixNano())
_, _, err = client.Repositories.CreateRepo(context.Background(), gitea.CreateRepoOption{Name: emptyRepoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(context.Background(), login.User, emptyRepoName); delErr != nil {
t.Logf("failed to delete empty wiki repo %q: %v", emptyRepoName, delErr)
}
})
emptyRepoSlug := fmt.Sprintf("%s/%s", login.User, emptyRepoName)
emptyListOutput := runTeaCommand(t,
"wiki", "list",
"--login", login.Name,
"--repo", emptyRepoSlug,
)
assert.Contains(t, emptyListOutput, "No wiki pages found")
viewOutput := runTeaCommand(t,
"wiki", "view", "Home",
"--login", login.Name,
"--repo", repoSlug,
"--output", "json",
)
assert.Contains(t, viewOutput, "\"title\": \"Home\"")
assert.Contains(t, viewOutput, "\"content\": \"# Home\"")
runTeaCommand(t,
"wiki", "edit", "Home",
"--login", login.Name,
"--repo", repoSlug,
"--content", "# Updated",
"--message", "edit home",
"--output", "json",
)
page, _, err = client.Wiki.GetPage(context.Background(), login.User, repoName, "Home")
require.NoError(t, err)
assert.Equal(t, "Home", page.Title)
assert.Contains(t, decodeBase64String(t, page.ContentBase64), "# Updated")
viewRenderedOutput := runTeaCommand(t,
"wiki", "view", "Home",
"--login", login.Name,
"--repo", repoSlug,
)
assert.Contains(t, viewRenderedOutput, "# Updated")
assert.NotContains(t, viewRenderedOutput, "Last updated")
revisionsOutput := runTeaCommand(t,
"wiki", "revisions", "Home",
"--login", login.Name,
"--repo", repoSlug,
"--output", "json",
)
assert.Contains(t, revisionsOutput, "create home")
assert.Contains(t, revisionsOutput, "edit home")
runTeaCommand(t,
"wiki", "delete", "Home",
"--login", login.Name,
"--repo", repoSlug,
"--confirm",
)
_, _, err = client.Wiki.GetPage(context.Background(), login.User, repoName, "Home")
require.Error(t, err)
assert.True(t, strings.Contains(strings.ToLower(err.Error()), "not found") || strings.Contains(strings.ToLower(err.Error()), "404"))
}
func runTeaCommand(t *testing.T, args ...string) string {
t.Helper()
app := teacmd.App()
errBuf := &bytes.Buffer{}
app.Writer = io.Discard
app.ErrWriter = errBuf
oldStdout := os.Stdout
reader, writer, err := os.Pipe()
require.NoError(t, err)
os.Stdout = writer
defer func() {
os.Stdout = oldStdout
}()
done := make(chan string, 1)
go func() {
var outBuf bytes.Buffer
_, _ = io.Copy(&outBuf, reader)
done <- outBuf.String()
}()
runErr := app.Run(context.Background(), append([]string{"tea"}, args...))
require.NoError(t, writer.Close())
output := <-done
require.NoError(t, reader.Close())
require.NoError(t, runErr, "tea %s failed with stdout: %s\nstderr: %s", strings.Join(args, " "), output, errBuf.String())
return output
}
func decodeBase64String(t *testing.T, encoded string) string {
t.Helper()
decoded, err := base64.StdEncoding.DecodeString(encoded)
require.NoError(t, err)
return string(decoded)
}