2025-12-16 12:17:07 +01:00
|
|
|
//go:build functional
|
2025-12-16 10:45:29 +01:00
|
|
|
// +build functional
|
|
|
|
|
|
|
|
|
|
package functional
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-16 12:17:07 +01:00
|
|
|
"bytes"
|
2025-12-16 10:45:29 +01:00
|
|
|
"fmt"
|
|
|
|
|
"os"
|
2025-12-16 12:17:07 +01:00
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
test: rewrite functional tests for full CLI coverage
Consolidate all tests into functional_test.go — remove duplicate
new_commands_test.go. Replace SDK-only tests with actual CLI binary
invocations. Add missing coverage for: issue list, issue view,
issue comment, issue create, issue edit title, repo view, repo list,
release view, --json flag on issue list/view and pr list.
All tests now use -R flag consistently.
35 pass, 0 fail, 3 expected skips (pr view/diff need PRs, clone needs auth).
2026-03-21 22:12:20 -06:00
|
|
|
"strings"
|
2025-12-16 10:45:29 +01:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"code.gitea.io/sdk/gitea"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestEnv holds the functional test environment
|
|
|
|
|
type TestEnv struct {
|
2025-12-16 12:17:07 +01:00
|
|
|
Token string
|
|
|
|
|
Hostname string
|
|
|
|
|
Owner string
|
|
|
|
|
RepoName string
|
|
|
|
|
Client *gitea.Client
|
|
|
|
|
T *testing.T
|
2025-12-16 10:45:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewTestEnv creates a new functional test environment from environment variables
|
|
|
|
|
func NewTestEnv(t *testing.T) *TestEnv {
|
|
|
|
|
token := os.Getenv("CODEBERG_TEST_TOKEN")
|
|
|
|
|
if token == "" {
|
|
|
|
|
t.Skip("CODEBERG_TEST_TOKEN not set, skipping functional tests")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hostname := os.Getenv("CODEBERG_TEST_HOSTNAME")
|
|
|
|
|
if hostname == "" {
|
|
|
|
|
hostname = "codeberg.org"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner := os.Getenv("CODEBERG_TEST_OWNER")
|
|
|
|
|
if owner == "" {
|
|
|
|
|
t.Fatal("CODEBERG_TEST_OWNER environment variable not set")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
repoName := os.Getenv("CODEBERG_TEST_REPO")
|
|
|
|
|
if repoName == "" {
|
|
|
|
|
t.Fatal("CODEBERG_TEST_REPO environment variable not set")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create Gitea client
|
|
|
|
|
client, err := gitea.NewClient(
|
|
|
|
|
fmt.Sprintf("https://%s", hostname),
|
|
|
|
|
gitea.SetToken(token),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create Gitea client: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
env := &TestEnv{
|
|
|
|
|
Token: token,
|
|
|
|
|
Hostname: hostname,
|
|
|
|
|
Owner: owner,
|
|
|
|
|
RepoName: repoName,
|
|
|
|
|
Client: client,
|
|
|
|
|
T: t,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify authentication by getting user info
|
|
|
|
|
user, _, err := client.GetMyUserInfo()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("authentication failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Logf("Authenticated as user: %s on %s", user.UserName, hostname)
|
|
|
|
|
return env
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateTestIssue creates a test issue and returns its number
|
|
|
|
|
func (env *TestEnv) CreateTestIssue(title, body string) int64 {
|
|
|
|
|
opts := gitea.CreateIssueOption{
|
|
|
|
|
Title: title,
|
|
|
|
|
Body: body,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
issue, _, err := env.Client.CreateIssue(env.Owner, env.RepoName, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Fatalf("failed to create test issue: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
env.T.Logf("Created test issue #%d", issue.Index)
|
|
|
|
|
return issue.Index
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CleanupIssue closes and deletes a test issue
|
|
|
|
|
func (env *TestEnv) CleanupIssue(issueNumber int64) {
|
|
|
|
|
closeState := gitea.StateClosed
|
|
|
|
|
_, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNumber, gitea.EditIssueOption{
|
|
|
|
|
State: &closeState,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Logf("warning: failed to close issue #%d: %v", issueNumber, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateTestPullRequest creates a test PR (requires a branch to exist)
|
2025-12-16 12:17:07 +01:00
|
|
|
// Note: Creating PRs requires multiple branches, so this is a placeholder for future use
|
2025-12-16 10:45:29 +01:00
|
|
|
func (env *TestEnv) CreateTestPullRequest(title, body, head, base string) int64 {
|
|
|
|
|
opts := gitea.CreatePullRequestOption{
|
|
|
|
|
Head: head,
|
|
|
|
|
Base: base,
|
|
|
|
|
Title: title,
|
|
|
|
|
Body: body,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pr, _, err := env.Client.CreatePullRequest(env.Owner, env.RepoName, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Fatalf("failed to create test PR: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
env.T.Logf("Created test PR #%d", pr.Index)
|
|
|
|
|
return pr.Index
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 12:17:07 +01:00
|
|
|
// ListPullRequests gets all PRs in the repository
|
|
|
|
|
func (env *TestEnv) ListPullRequests() ([]*gitea.PullRequest, error) {
|
|
|
|
|
prs, _, err := env.Client.ListRepoPullRequests(env.Owner, env.RepoName, gitea.ListPullRequestsOptions{})
|
|
|
|
|
return prs, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPullRequest gets a specific PR
|
|
|
|
|
func (env *TestEnv) GetPullRequest(prNumber int64) (*gitea.PullRequest, error) {
|
|
|
|
|
pr, _, err := env.Client.GetPullRequest(env.Owner, env.RepoName, prNumber)
|
|
|
|
|
return pr, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 10:45:29 +01:00
|
|
|
// CleanupPullRequest closes a test PR
|
|
|
|
|
func (env *TestEnv) CleanupPullRequest(prNumber int64) {
|
|
|
|
|
closed := gitea.StateClosed
|
|
|
|
|
_, _, err := env.Client.EditPullRequest(env.Owner, env.RepoName, prNumber, gitea.EditPullRequestOption{
|
|
|
|
|
State: &closed,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Logf("warning: failed to close PR #%d: %v", prNumber, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// VerifyAPIConnection verifies the test environment can connect to the API
|
|
|
|
|
func (env *TestEnv) VerifyAPIConnection() {
|
|
|
|
|
_, _, err := env.Client.GetMyUserInfo()
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Fatalf("failed to verify API connection: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-16 12:17:07 +01:00
|
|
|
|
2026-02-20 17:48:56 +01:00
|
|
|
// EnsureTestLabels creates test labels if they don't exist
|
|
|
|
|
func (env *TestEnv) EnsureTestLabels() {
|
|
|
|
|
labelNames := []string{"bug", "enhancement", "help-wanted"}
|
|
|
|
|
|
|
|
|
|
for _, name := range labelNames {
|
|
|
|
|
labels, _, err := env.Client.ListRepoLabels(env.Owner, env.RepoName, gitea.ListLabelsOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Fatalf("failed to list labels: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exists := false
|
|
|
|
|
for _, label := range labels {
|
|
|
|
|
if label.Name == name {
|
|
|
|
|
exists = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
|
color := "#00aabb"
|
|
|
|
|
if name == "bug" {
|
|
|
|
|
color = "#ff0000"
|
|
|
|
|
} else if name == "enhancement" {
|
|
|
|
|
color = "#00ff00"
|
|
|
|
|
}
|
|
|
|
|
_, _, err = env.Client.CreateLabel(env.Owner, env.RepoName, gitea.CreateLabelOption{
|
|
|
|
|
Name: name,
|
|
|
|
|
Color: color,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Logf("warning: failed to create label '%s': %v", name, err)
|
|
|
|
|
} else {
|
|
|
|
|
env.T.Logf("Created test label: %s", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetIssueLabels gets the labels on an issue
|
|
|
|
|
func (env *TestEnv) GetIssueLabels(issueNumber int64) ([]*gitea.Label, error) {
|
|
|
|
|
labels, _, err := env.Client.GetIssueLabels(env.Owner, env.RepoName, issueNumber, gitea.ListLabelsOptions{})
|
|
|
|
|
return labels, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetLabelIDs converts label names to label IDs
|
|
|
|
|
func (env *TestEnv) GetLabelIDs(labelNames []string) []int64 {
|
|
|
|
|
labels, _, err := env.Client.ListRepoLabels(env.Owner, env.RepoName, gitea.ListLabelsOptions{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Fatalf("failed to list labels: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nameToID := make(map[string]int64)
|
|
|
|
|
for _, label := range labels {
|
|
|
|
|
nameToID[label.Name] = label.ID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var ids []int64
|
|
|
|
|
for _, name := range labelNames {
|
|
|
|
|
id, exists := nameToID[name]
|
|
|
|
|
if !exists {
|
|
|
|
|
env.T.Fatalf("label '%s' not found", name)
|
|
|
|
|
}
|
|
|
|
|
ids = append(ids, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ids
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 15:40:22 +01:00
|
|
|
// CleanupRepo deletes a repository created during testing.
|
|
|
|
|
func (env *TestEnv) CleanupRepo(owner, repoName string) {
|
|
|
|
|
_, err := env.Client.DeleteRepo(owner, repoName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
env.T.Logf("warning: failed to delete repo %s/%s: %v", owner, repoName, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 08:16:52 -06:00
|
|
|
// GetBinaryPath returns the path to the built fj binary
|
2025-12-16 12:17:07 +01:00
|
|
|
func (env *TestEnv) GetBinaryPath() string {
|
|
|
|
|
binaryPath := os.Getenv("FGJ_BINARY_PATH")
|
|
|
|
|
if binaryPath == "" {
|
|
|
|
|
// Look for the binary in common locations
|
|
|
|
|
candidates := []string{
|
2026-04-26 08:16:52 -06:00
|
|
|
"./bin/fj",
|
|
|
|
|
"bin/fj",
|
|
|
|
|
"/home/romain/work/fj/bin/fj",
|
2025-12-16 12:17:07 +01:00
|
|
|
}
|
|
|
|
|
for _, candidate := range candidates {
|
|
|
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
|
|
|
// Convert to absolute path
|
|
|
|
|
if abs, err := filepath.Abs(candidate); err == nil {
|
|
|
|
|
return abs
|
|
|
|
|
}
|
|
|
|
|
return candidate
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If no binary found, return default (will error when executed)
|
2026-04-26 08:16:52 -06:00
|
|
|
binaryPath = "./bin/fj"
|
2025-12-16 12:17:07 +01:00
|
|
|
}
|
|
|
|
|
return binaryPath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CLIResult holds the result of a CLI command execution
|
|
|
|
|
type CLIResult struct {
|
|
|
|
|
Stdout string
|
|
|
|
|
Stderr string
|
|
|
|
|
ExitCode int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RunCLI executes a CLI command and returns stdout, stderr, and exit code
|
|
|
|
|
func (env *TestEnv) RunCLI(args ...string) *CLIResult {
|
|
|
|
|
cmd := exec.Command(env.GetBinaryPath(), args...)
|
|
|
|
|
|
|
|
|
|
// Set up environment with test credentials
|
|
|
|
|
cmd.Env = os.Environ()
|
|
|
|
|
|
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
|
|
|
|
|
|
err := cmd.Run()
|
|
|
|
|
exitCode := 0
|
|
|
|
|
if err != nil {
|
|
|
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
|
|
|
exitCode = exitErr.ExitCode()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := &CLIResult{
|
|
|
|
|
Stdout: stdout.String(),
|
|
|
|
|
Stderr: stderr.String(),
|
|
|
|
|
ExitCode: exitCode,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
env.T.Logf("Command: %s %v", env.GetBinaryPath(), args)
|
|
|
|
|
env.T.Logf("Exit code: %d", exitCode)
|
|
|
|
|
if stdout.Len() > 0 {
|
|
|
|
|
env.T.Logf("Stdout:\n%s", result.Stdout)
|
|
|
|
|
}
|
|
|
|
|
if stderr.Len() > 0 {
|
|
|
|
|
env.T.Logf("Stderr:\n%s", result.Stderr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
test: rewrite functional tests for full CLI coverage
Consolidate all tests into functional_test.go — remove duplicate
new_commands_test.go. Replace SDK-only tests with actual CLI binary
invocations. Add missing coverage for: issue list, issue view,
issue comment, issue create, issue edit title, repo view, repo list,
release view, --json flag on issue list/view and pr list.
All tests now use -R flag consistently.
35 pass, 0 fail, 3 expected skips (pr view/diff need PRs, clone needs auth).
2026-03-21 22:12:20 -06:00
|
|
|
|
|
|
|
|
// runCLIWithStdin executes the CLI binary with the given args and pipes input to stdin.
|
|
|
|
|
func (env *TestEnv) runCLIWithStdin(input string, args ...string) *CLIResult {
|
|
|
|
|
cmd := exec.Command(env.GetBinaryPath(), args...)
|
|
|
|
|
cmd.Env = os.Environ()
|
|
|
|
|
cmd.Stdin = strings.NewReader(input)
|
|
|
|
|
|
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
|
|
|
|
|
|
err := cmd.Run()
|
|
|
|
|
exitCode := 0
|
|
|
|
|
if err != nil {
|
|
|
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
|
|
|
exitCode = exitErr.ExitCode()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := &CLIResult{
|
|
|
|
|
Stdout: stdout.String(),
|
|
|
|
|
Stderr: stderr.String(),
|
|
|
|
|
ExitCode: exitCode,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
env.T.Logf("Command: %s %v", env.GetBinaryPath(), args)
|
|
|
|
|
env.T.Logf("Exit code: %d", exitCode)
|
|
|
|
|
if stdout.Len() > 0 {
|
|
|
|
|
env.T.Logf("Stdout:\n%s", result.Stdout)
|
|
|
|
|
}
|
|
|
|
|
if stderr.Len() > 0 {
|
|
|
|
|
env.T.Logf("Stderr:\n%s", result.Stderr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|