diff --git a/tests/functional/fixtures.go b/tests/functional/fixtures.go index 5423e5b..6bf9733 100644 --- a/tests/functional/fixtures.go +++ b/tests/functional/fixtures.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" "code.gitea.io/sdk/gitea" @@ -295,3 +296,39 @@ func (env *TestEnv) RunCLI(args ...string) *CLIResult { return result } + +// 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 +} diff --git a/tests/functional/functional_test.go b/tests/functional/functional_test.go index 3803e56..7202bb5 100644 --- a/tests/functional/functional_test.go +++ b/tests/functional/functional_test.go @@ -5,21 +5,23 @@ package functional import ( "bytes" + "encoding/json" "fmt" "os" + "strings" "testing" "time" "code.gitea.io/sdk/gitea" ) -// TestAPIConnection verifies we can connect to the Codeberg API with the test account +// ===== SDK API tests (verify test environment and API connectivity) ===== + func TestAPIConnection(t *testing.T) { env := NewTestEnv(t) env.VerifyAPIConnection() } -// TestListRepositories verifies we can list repositories func TestListRepositories(t *testing.T) { env := NewTestEnv(t) @@ -34,7 +36,6 @@ func TestListRepositories(t *testing.T) { t.Logf("Found %d repositories", len(repos)) - // Verify we can find our test repo found := false for _, repo := range repos { if repo.Name == env.RepoName { @@ -49,7 +50,6 @@ func TestListRepositories(t *testing.T) { } } -// TestGetRepository verifies we can get repository details func TestGetRepository(t *testing.T) { env := NewTestEnv(t) @@ -69,50 +69,165 @@ func TestGetRepository(t *testing.T) { t.Logf("Repository: %s (%s)", repo.FullName, repo.Description) } -// TestCreateAndListIssues verifies we can create and list issues -func TestCreateAndListIssues(t *testing.T) { +func TestAPIErrorHandling(t *testing.T) { env := NewTestEnv(t) - // Create a test issue - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Test Issue", - "This is a functional test issue for fgj", - ) - - defer env.CleanupIssue(issueNum) - - // List issues - issues, _, err := env.Client.ListRepoIssues(env.Owner, env.RepoName, gitea.ListIssueOption{}) - if err != nil { - t.Fatalf("failed to list issues: %v", err) + _, _, err := env.Client.GetIssue(env.Owner, env.RepoName, 999999) + if err == nil { + t.Fatalf("expected error for non-existent issue, got none") } - // Verify our created issue is in the list - found := false - for _, issue := range issues { - if issue.Index == issueNum { - found = true + t.Logf("Properly handled error: %v", err) +} + +// ===== CLI Issue Commands ===== + +func TestCLIIssueList(t *testing.T) { + env := NewTestEnv(t) + + // Create a test issue so the list is not empty + issueNum := env.CreateTestIssue("[FGJ E2E Test] Issue List", "For issue list test") + defer env.CleanupIssue(issueNum) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "list", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue list failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + if !strings.Contains(result.Stdout, "Issue List") { + t.Fatalf("expected issue list output to contain our test issue, got: %s", result.Stdout) + } + + t.Logf("Successfully listed issues via CLI") +} + +func TestCLIIssueListJSON(t *testing.T) { + env := NewTestEnv(t) + + issueNum := env.CreateTestIssue("[FGJ E2E Test] JSON List", "For JSON output test") + defer env.CleanupIssue(issueNum) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "list", "--json", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue list --json failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + var issues []interface{} + if err := json.Unmarshal([]byte(result.Stdout), &issues); err != nil { + t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, result.Stdout) + } + + if len(issues) == 0 { + t.Fatalf("expected at least one issue in JSON output") + } + + t.Logf("Successfully listed %d issues via CLI with --json", len(issues)) +} + +func TestCLIIssueView(t *testing.T) { + env := NewTestEnv(t) + + issueNum := env.CreateTestIssue("[FGJ E2E Test] View Test", "Testing issue view") + defer env.CleanupIssue(issueNum) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "view", fmt.Sprintf("%d", issueNum), + ) + + if result.ExitCode != 0 { + t.Fatalf("issue view failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + if !strings.Contains(result.Stdout, "View Test") { + t.Fatalf("expected issue view output to contain title, got: %s", result.Stdout) + } + + t.Logf("Successfully viewed issue #%d via CLI", issueNum) +} + +func TestCLIIssueViewJSON(t *testing.T) { + env := NewTestEnv(t) + + issueNum := env.CreateTestIssue("[FGJ E2E Test] JSON View", "Testing JSON view") + defer env.CleanupIssue(issueNum) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "view", fmt.Sprintf("%d", issueNum), "--json", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue view --json failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + var data map[string]interface{} + if err := json.Unmarshal([]byte(result.Stdout), &data); err != nil { + t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, result.Stdout) + } + + // issue view --json wraps the issue in an "issue" key + issueObj, _ := data["issue"].(map[string]interface{}) + if issueObj == nil { + // fallback: maybe it's flat + issueObj = data + } + if title, ok := issueObj["title"].(string); !ok || !strings.Contains(title, "JSON View") { + t.Fatalf("expected title containing 'JSON View' in JSON output, got: %v", issueObj["title"]) + } + + t.Logf("Successfully viewed issue #%d via CLI with --json", issueNum) +} + +func TestCLIIssueCreate(t *testing.T) { + env := NewTestEnv(t) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "create", + "-t", "[FGJ E2E Test] CLI Created Issue", + "-b", "Created directly via fgj CLI", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue create failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + var issueNum int64 + for _, line := range strings.Split(strings.TrimSpace(result.Stdout), "\n") { + if _, err := fmt.Sscanf(line, "Issue created: #%d", &issueNum); err == nil { break } } - - if !found { - t.Fatalf("created issue #%d not found in issue list", issueNum) + if issueNum == 0 { + t.Fatalf("failed to parse issue number from output: %s", result.Stdout) } - t.Logf("Successfully created and listed issue #%d", issueNum) + defer env.CleanupIssue(issueNum) + t.Logf("Successfully created issue #%d via CLI", issueNum) } -// TestIssueCreateWithLabels verifies we can create issues with labels via CLI -func TestIssueCreateWithLabels(t *testing.T) { +func TestCLIIssueCreateWithLabels(t *testing.T) { env := NewTestEnv(t) - // Ensure test labels exist env.EnsureTestLabels() - // Create an issue with labels via CLI result := env.RunCLI( "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "issue", "create", "-t", "[FGJ E2E Test] Issue with Labels", "-b", "This issue was created with labels", @@ -124,16 +239,18 @@ func TestIssueCreateWithLabels(t *testing.T) { t.Fatalf("issue create with labels failed with exit code %d: %s", result.ExitCode, result.Stderr) } - // Extract issue number from output (format: "Issue created: #") var issueNum int64 - _, err := fmt.Sscanf(result.Stdout, "Issue created: #%d", &issueNum) - if err != nil { - t.Fatalf("failed to parse issue number from output: %v", err) + for _, line := range strings.Split(strings.TrimSpace(result.Stdout), "\n") { + if _, err := fmt.Sscanf(line, "Issue created: #%d", &issueNum); err == nil { + break + } + } + if issueNum == 0 { + t.Fatalf("failed to parse issue number from output: %s", result.Stdout) } defer env.CleanupIssue(issueNum) - // Verify labels were applied labels, err := env.GetIssueLabels(issueNum) if err != nil { t.Fatalf("failed to get issue labels: %v", err) @@ -155,33 +272,161 @@ func TestIssueCreateWithLabels(t *testing.T) { t.Logf("Successfully created issue #%d with labels: bug, enhancement", issueNum) } -// TestIssueEditAddLabels verifies we can add labels to an issue via CLI -func TestIssueEditAddLabels(t *testing.T) { +func TestCLIIssueComment(t *testing.T) { env := NewTestEnv(t) - // Ensure test labels exist - env.EnsureTestLabels() - - // Create an issue without labels - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Add Labels Test", - "This issue will have labels added", - ) + issueNum := env.CreateTestIssue("[FGJ E2E Test] Comment Test", "Testing comment via CLI") defer env.CleanupIssue(issueNum) - // Verify no labels initially - labels, err := env.GetIssueLabels(issueNum) - if err != nil { - t.Fatalf("failed to get initial labels: %v", err) - } - - if len(labels) != 0 { - t.Fatalf("expected 0 labels initially, got %d", len(labels)) - } - - // Add labels via CLI result := env.RunCLI( "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "comment", + fmt.Sprintf("%d", issueNum), + "-b", "CLI comment from functional test", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue comment failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + // Verify comment was added via API + comments, _, err := env.Client.ListIssueComments(env.Owner, env.RepoName, issueNum, gitea.ListIssueCommentOptions{}) + if err != nil { + t.Fatalf("failed to list comments: %v", err) + } + + found := false + for _, c := range comments { + if c.Body == "CLI comment from functional test" { + found = true + break + } + } + if !found { + t.Fatalf("comment not found on issue #%d", issueNum) + } + + t.Logf("Successfully commented on issue #%d via CLI", issueNum) +} + +func TestCLIIssueClose(t *testing.T) { + env := NewTestEnv(t) + + issueNum := env.CreateTestIssue("[FGJ E2E Test] Close Test", "Will be closed via CLI") + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "close", + fmt.Sprintf("%d", issueNum), + ) + + if result.ExitCode != 0 { + t.Fatalf("issue close failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.State != "closed" { + t.Fatalf("expected issue state 'closed', got '%s'", issue.State) + } + + t.Logf("Successfully closed issue #%d via CLI", issueNum) +} + +func TestCLIIssueCloseWithComment(t *testing.T) { + env := NewTestEnv(t) + + issueNum := env.CreateTestIssue("[FGJ E2E Test] Close with comment", "Will be closed with a comment") + + commentText := "Fixed in v2.0 - closing via functional test" + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "close", + fmt.Sprintf("%d", issueNum), + "-c", commentText, + ) + + if result.ExitCode != 0 { + t.Fatalf("issue close -c failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.State != "closed" { + t.Fatalf("expected issue state 'closed', got '%s'", issue.State) + } + + comments, _, err := env.Client.ListIssueComments(env.Owner, env.RepoName, issueNum, gitea.ListIssueCommentOptions{}) + if err != nil { + t.Fatalf("failed to list issue comments: %v", err) + } + + found := false + for _, comment := range comments { + if comment.Body == commentText { + found = true + break + } + } + + if !found { + t.Fatalf("comment '%s' not found in issue comments", commentText) + } + + t.Logf("Successfully closed issue #%d with comment via CLI", issueNum) +} + +func TestCLIIssueEditTitle(t *testing.T) { + env := NewTestEnv(t) + + issueNum := env.CreateTestIssue("[FGJ E2E Test] Original Title", "Will be edited") + defer env.CleanupIssue(issueNum) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "edit", + fmt.Sprintf("%d", issueNum), + "-t", "[FGJ E2E Test] Updated Title", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue edit failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.Title != "[FGJ E2E Test] Updated Title" { + t.Fatalf("expected updated title, got '%s'", issue.Title) + } + + t.Logf("Successfully edited issue #%d title via CLI", issueNum) +} + +func TestCLIIssueEditAddLabels(t *testing.T) { + env := NewTestEnv(t) + + env.EnsureTestLabels() + + issueNum := env.CreateTestIssue("[FGJ E2E Test] Add Labels", "Will have labels added") + defer env.CleanupIssue(issueNum) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "issue", "edit", fmt.Sprintf("%d", issueNum), "--add-label", "bug", @@ -192,8 +437,7 @@ func TestIssueEditAddLabels(t *testing.T) { t.Fatalf("issue edit add-label failed with exit code %d: %s", result.ExitCode, result.Stderr) } - // Verify labels were added - labels, err = env.GetIssueLabels(issueNum) + labels, err := env.GetIssueLabels(issueNum) if err != nil { t.Fatalf("failed to get labels after edit: %v", err) } @@ -211,27 +455,23 @@ func TestIssueEditAddLabels(t *testing.T) { t.Fatalf("expected labels 'bug' and 'help-wanted', got %v", labelNames) } - t.Logf("Successfully added labels to issue #%d", issueNum) + t.Logf("Successfully added labels to issue #%d via CLI", issueNum) } -// TestIssueEditRemoveLabels verifies we can remove labels from an issue via CLI -func TestIssueEditRemoveLabels(t *testing.T) { +func TestCLIIssueEditRemoveLabels(t *testing.T) { env := NewTestEnv(t) - // Ensure test labels exist env.EnsureTestLabels() - // Create an issue with labels issue, _, err := env.Client.CreateIssue(env.Owner, env.RepoName, gitea.CreateIssueOption{ - Title: "[FGJ E2E Test] Remove Labels Test", - Body: "This issue will have labels removed", + Title: "[FGJ E2E Test] Remove Labels", + Body: "Will have labels removed", }) if err != nil { t.Fatalf("failed to create issue: %v", err) } defer env.CleanupIssue(issue.Index) - // Add labels via API labelIDs := env.GetLabelIDs([]string{"bug", "enhancement"}) _, _, err = env.Client.AddIssueLabels(env.Owner, env.RepoName, issue.Index, gitea.IssueLabelsOption{ Labels: labelIDs, @@ -240,19 +480,9 @@ func TestIssueEditRemoveLabels(t *testing.T) { t.Fatalf("failed to add initial labels: %v", err) } - // Verify initial labels - labels, err := env.GetIssueLabels(issue.Index) - if err != nil { - t.Fatalf("failed to get initial labels: %v", err) - } - - if len(labels) != 2 { - t.Fatalf("expected 2 labels initially, got %d", len(labels)) - } - - // Remove one label via CLI result := env.RunCLI( "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "issue", "edit", fmt.Sprintf("%d", issue.Index), "--remove-label", "bug", @@ -262,8 +492,7 @@ func TestIssueEditRemoveLabels(t *testing.T) { t.Fatalf("issue edit remove-label failed with exit code %d: %s", result.ExitCode, result.Stderr) } - // Verify label was removed - labels, err = env.GetIssueLabels(issue.Index) + labels, err := env.GetIssueLabels(issue.Index) if err != nil { t.Fatalf("failed to get labels after edit: %v", err) } @@ -276,455 +505,17 @@ func TestIssueEditRemoveLabels(t *testing.T) { t.Fatalf("expected remaining label 'enhancement', got '%s'", labels[0].Name) } - t.Logf("Successfully removed label from issue #%d", issue.Index) + t.Logf("Successfully removed label from issue #%d via CLI", issue.Index) } -// TestGetIssue verifies we can get issue details -func TestGetIssue(t *testing.T) { - env := NewTestEnv(t) +// ===== CLI PR Commands ===== - // Create a test issue - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Get Issue Test", - "Testing get issue functionality", - ) - - defer env.CleanupIssue(issueNum) - - // Get the issue - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.Index != issueNum { - t.Fatalf("expected issue number %d, got %d", issueNum, issue.Index) - } - - if issue.Title != "[FGJ E2E Test] Get Issue Test" { - t.Fatalf("issue title mismatch") - } - - t.Logf("Retrieved issue #%d: %s", issueNum, issue.Title) -} - -// TestCommentOnIssue verifies we can comment on issues -func TestCommentOnIssue(t *testing.T) { - env := NewTestEnv(t) - - // Create a test issue - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Comment Test", - "Testing comment functionality", - ) - - defer env.CleanupIssue(issueNum) - - // Add a comment - comment, _, err := env.Client.CreateIssueComment( - env.Owner, - env.RepoName, - issueNum, - gitea.CreateIssueCommentOption{ - Body: "This is an automated test comment from fgj functional tests", - }, - ) - if err != nil { - t.Fatalf("failed to create issue comment: %v", err) - } - - if comment.Body != "This is an automated test comment from fgj functional tests" { - t.Fatalf("comment body mismatch") - } - - t.Logf("Successfully added comment to issue #%d", issueNum) -} - -// TestGetRepositoryWithCollaborators verifies we can get repo and check collaborators -func TestGetRepositoryWithCollaborators(t *testing.T) { - env := NewTestEnv(t) - - // Get repository with full details - repo, _, err := env.Client.GetRepo(env.Owner, env.RepoName) - if err != nil { - t.Fatalf("failed to get repository: %v", err) - } - - // Verify basic repo properties - if repo.FullName != env.Owner+"/"+env.RepoName { - t.Fatalf("expected repo full name %s/%s, got %s", env.Owner, env.RepoName, repo.FullName) - } - - t.Logf("Repository %s has %d watchers, %d stars, %d forks", - repo.FullName, repo.Watchers, repo.Stars, repo.Forks) -} - -// TestAPIErrorHandling verifies proper error handling for invalid requests -func TestAPIErrorHandling(t *testing.T) { - env := NewTestEnv(t) - - // Try to get a non-existent issue - _, _, err := env.Client.GetIssue(env.Owner, env.RepoName, 999999) - if err == nil { - t.Fatalf("expected error for non-existent issue, got none") - } - - t.Logf("Properly handled error: %v", err) -} - -// TestRepositoryExists verifies the test repository exists and is accessible -func TestRepositoryExists(t *testing.T) { - env := NewTestEnv(t) - - repo, _, err := env.Client.GetRepo(env.Owner, env.RepoName) - if err != nil { - t.Fatalf("test repository does not exist or is not accessible: %v", err) - } - - if repo.Private { - t.Logf("Test repository is private: %s", repo.FullName) - } else { - t.Logf("Test repository is public: %s", repo.FullName) - } -} - -// ===== CLI COMMAND TESTS ===== - -// TestCLIIssueCreate verifies the `fgj issue create` command works (via API, not CLI) -// Note: CLI requires proper config setup which is tested via API tests -func TestCLIIssueCreate(t *testing.T) { - env := NewTestEnv(t) - - // Use API directly since CLI requires config file for auth - issueNum := env.CreateTestIssue( - "[FGJ CLI Test] Created via API", - "This issue was created using the API (CLI test proxy)", - ) - defer env.CleanupIssue(issueNum) - - // Verify the issue was created - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get created issue: %v", err) - } - - if issue.Title != "[FGJ CLI Test] Created via API" { - t.Fatalf("issue title mismatch") - } - - t.Logf("Successfully tested issue create via API for CLI #%d", issueNum) -} - -// TestCLIIssueComment verifies the `fgj issue comment` command works (via API, not CLI) -// Note: CLI requires proper config setup which is tested via API tests -func TestCLIIssueComment(t *testing.T) { - env := NewTestEnv(t) - - // Create an issue via API - issueNum := env.CreateTestIssue( - "[FGJ CLI Test] For commenting", - "This issue will receive a comment", - ) - defer env.CleanupIssue(issueNum) - - // Add comment via API - comment, _, err := env.Client.CreateIssueComment( - env.Owner, - env.RepoName, - issueNum, - gitea.CreateIssueCommentOption{ - Body: "This is a test comment", - }, - ) - if err != nil { - t.Fatalf("failed to create comment: %v", err) - } - - if comment.Body != "This is a test comment" { - t.Fatalf("comment body mismatch") - } - - t.Logf("Successfully tested issue comment via API for CLI #%d", issueNum) -} - -// TestCLIIssueClose verifies the `fgj issue close` command works (via API, not CLI) -// Note: CLI requires proper config setup which is tested via API tests -func TestCLIIssueClose(t *testing.T) { - env := NewTestEnv(t) - - // Create an issue via API - issueNum := env.CreateTestIssue( - "[FGJ CLI Test] For closing", - "This issue will be closed", - ) - - // Close via API - closeState := gitea.StateClosed - _, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ - State: &closeState, - }) - if err != nil { - t.Fatalf("failed to close issue: %v", err) - } - - // Verify the issue was closed - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.State != "closed" { - t.Fatalf("expected issue state 'closed', got '%s'", issue.State) - } - - t.Logf("Successfully tested issue close via API for CLI #%d", issueNum) -} - -// TestCLIIssueCloseWithComment verifies the `fgj issue close -c` command works -func TestCLIIssueCloseWithComment(t *testing.T) { - env := NewTestEnv(t) - - // Create an issue via API - issueNum := env.CreateTestIssue( - "[FGJ CLI Test] Close with comment", - "This issue will be closed with a comment in one command", - ) - - commentText := "Fixed in v2.0 - closing via functional test" - - // Close with comment via CLI - result := env.RunCLI( - "--hostname", env.Hostname, - "issue", "close", - fmt.Sprintf("%d", issueNum), - "-c", commentText, - ) - - if result.ExitCode != 0 { - t.Fatalf("issue close -c failed with exit code %d: %s", result.ExitCode, result.Stderr) - } - - // Verify the issue was closed - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.State != "closed" { - t.Fatalf("expected issue state 'closed', got '%s'", issue.State) - } - - // Verify the comment was added - comments, _, err := env.Client.ListIssueComments(env.Owner, env.RepoName, issueNum, gitea.ListIssueCommentOptions{}) - if err != nil { - t.Fatalf("failed to list issue comments: %v", err) - } - - if len(comments) == 0 { - t.Fatalf("expected at least one comment, got none") - } - - // Find our comment - found := false - for _, comment := range comments { - if comment.Body == commentText { - found = true - break - } - } - - if !found { - t.Fatalf("comment '%s' not found in issue comments", commentText) - } - - t.Logf("Successfully tested issue close with comment via CLI #%d", issueNum) -} - -// TestEditIssueTitle verifies we can edit an issue's title -func TestEditIssueTitle(t *testing.T) { - env := NewTestEnv(t) - - // Create a test issue - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Original Title", - "This issue's title will be edited", - ) - - defer env.CleanupIssue(issueNum) - - // Edit the title - newTitle := "[FGJ E2E Test] Updated Title" - _, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ - Title: newTitle, - }) - if err != nil { - t.Fatalf("failed to edit issue title: %v", err) - } - - // Verify the title was updated - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.Title != newTitle { - t.Fatalf("expected title '%s', got '%s'", newTitle, issue.Title) - } - - t.Logf("Successfully edited issue #%d title", issueNum) -} - -// TestEditIssueBody verifies we can edit an issue's body -func TestEditIssueBody(t *testing.T) { - env := NewTestEnv(t) - - // Create a test issue - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Edit Body Test", - "Original body content", - ) - - defer env.CleanupIssue(issueNum) - - // Edit the body - newBody := "Updated body content from functional test" - _, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ - Body: &newBody, - }) - if err != nil { - t.Fatalf("failed to edit issue body: %v", err) - } - - // Verify the body was updated - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.Body != newBody { - t.Fatalf("expected body '%s', got '%s'", newBody, issue.Body) - } - - t.Logf("Successfully edited issue #%d body", issueNum) -} - -// TestEditIssueState verifies we can edit an issue's state -func TestEditIssueState(t *testing.T) { - env := NewTestEnv(t) - - // Create a test issue (starts as open) - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Edit State Test", - "This issue's state will be changed", - ) - - defer env.CleanupIssue(issueNum) - - // Verify it starts as open - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.State != "open" { - t.Fatalf("expected initial state 'open', got '%s'", issue.State) - } - - // Edit state to closed - closedState := gitea.StateClosed - _, _, err = env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ - State: &closedState, - }) - if err != nil { - t.Fatalf("failed to edit issue state: %v", err) - } - - // Verify state changed to closed - issue, _, err = env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.State != "closed" { - t.Fatalf("expected state 'closed' after edit, got '%s'", issue.State) - } - - // Edit state back to open - openState := gitea.StateOpen - _, _, err = env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ - State: &openState, - }) - if err != nil { - t.Fatalf("failed to reopen issue: %v", err) - } - - // Verify state is now open - issue, _, err = env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.State != "open" { - t.Fatalf("expected state 'open' after reopen, got '%s'", issue.State) - } - - t.Logf("Successfully edited issue #%d state", issueNum) -} - -// TestEditIssueMultipleFields verifies we can edit multiple issue fields at once -func TestEditIssueMultipleFields(t *testing.T) { - env := NewTestEnv(t) - - // Create a test issue - issueNum := env.CreateTestIssue( - "[FGJ E2E Test] Original Multi-Edit", - "Original body for multi-field edit", - ) - - defer env.CleanupIssue(issueNum) - - // Edit title, body, and state at once - newTitle := "[FGJ E2E Test] Updated Multi-Edit" - newBody := "Updated body from multi-field edit test" - closedState := gitea.StateClosed - - _, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ - Title: newTitle, - Body: &newBody, - State: &closedState, - }) - if err != nil { - t.Fatalf("failed to edit multiple issue fields: %v", err) - } - - // Verify all fields were updated - issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) - if err != nil { - t.Fatalf("failed to get issue: %v", err) - } - - if issue.Title != newTitle { - t.Fatalf("expected title '%s', got '%s'", newTitle, issue.Title) - } - - if issue.Body != newBody { - t.Fatalf("expected body '%s', got '%s'", newBody, issue.Body) - } - - if issue.State != "closed" { - t.Fatalf("expected state 'closed', got '%s'", issue.State) - } - - t.Logf("Successfully edited multiple fields on issue #%d", issueNum) -} - -// TestCLIPRList verifies the `fgj pr list` command works func TestCLIPRList(t *testing.T) { env := NewTestEnv(t) - // Run: fgj pr list result := env.RunCLI( "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "pr", "list", ) @@ -732,72 +523,47 @@ func TestCLIPRList(t *testing.T) { t.Fatalf("pr list failed with exit code %d: %s", result.ExitCode, result.Stderr) } - // pr list should produce output (even if empty) - if result.Stdout == "" { - t.Logf("Note: pr list produced empty output (no PRs in repo)") - } else { - t.Logf("pr list output:\n%s", result.Stdout) - } - - // Verify we got some output (at least the header or "No pull requests" message) - if result.Stdout == "" && result.Stderr == "" { - t.Logf("Note: pr list produced no output") - } - t.Logf("Successfully listed pull requests via CLI") } -// TestCLIActionsRunList verifies the `fgj actions run list` command works -func TestCLIActionsRunList(t *testing.T) { +func TestCLIPRListJSON(t *testing.T) { env := NewTestEnv(t) - // Run: fgj actions run list result := env.RunCLI( "--hostname", env.Hostname, - "actions", "run", "list", + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "pr", "list", "--json", ) - // Actions might not be enabled, so we accept both success and failure if result.ExitCode != 0 { - if bytes.Contains([]byte(result.Stderr), []byte("Actions")) || - bytes.Contains([]byte(result.Stderr), []byte("not enabled")) || - bytes.Contains([]byte(result.Stderr), []byte("404")) { - t.Logf("Note: actions run list not available (Actions may not be enabled): %s", result.Stderr) - return - } - t.Logf("actions run list exited with code %d: %s", result.ExitCode, result.Stderr) + t.Fatalf("pr list --json failed with exit code %d: %s", result.ExitCode, result.Stderr) } - if result.Stdout == "" { - t.Logf("Note: actions run list produced empty output (no workflow runs)") - } else { - t.Logf("actions run list output:\n%s", result.Stdout) + var prs []interface{} + if err := json.Unmarshal([]byte(result.Stdout), &prs); err != nil { + t.Fatalf("failed to parse JSON output: %v\nOutput: %s", err, result.Stdout) } - t.Logf("Successfully listed workflow runs via CLI") + t.Logf("Successfully listed %d PRs via CLI with --json", len(prs)) } -// TestCLIPRView verifies the `fgj pr view` command works func TestCLIPRView(t *testing.T) { env := NewTestEnv(t) - // First, check if there are any PRs in the repository prs, err := env.ListPullRequests() if err != nil { t.Fatalf("failed to list PRs: %v", err) } if len(prs) == 0 { - t.Logf("Note: No pull requests in test repository, skipping pr view test") t.Skip("No PRs available to view") } - // Get the first PR number prNumber := prs[0].Index - // Run: fgj pr view result := env.RunCLI( "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "pr", "view", fmt.Sprintf("%d", prNumber), ) @@ -810,56 +576,226 @@ func TestCLIPRView(t *testing.T) { t.Fatalf("pr view produced no output") } - // Verify output contains PR information - if !bytes.Contains([]byte(result.Stdout), []byte(prs[0].Title)) && - !bytes.Contains([]byte(result.Stdout), []byte(fmt.Sprintf("#%d", prNumber))) { - t.Logf("Warning: pr view output may not contain expected PR info") - t.Logf("Output: %s", result.Stdout) - } - t.Logf("Successfully viewed PR #%d via CLI", prNumber) } -// TestCLIRepoClone verifies the `fgj repo clone` command works +func TestCLIPRDiff(t *testing.T) { + env := NewTestEnv(t) + + prs, err := env.ListPullRequests() + if err != nil { + t.Fatalf("failed to list PRs: %v", err) + } + + if len(prs) == 0 { + t.Skip("No PRs available for diff test") + } + + prNumber := prs[0].Index + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "pr", "diff", + fmt.Sprintf("%d", prNumber), + "--name-only", + ) + + if result.ExitCode != 0 { + t.Fatalf("pr diff failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + if strings.TrimSpace(result.Stdout) == "" { + t.Fatalf("pr diff --name-only produced no output") + } + + t.Logf("Successfully retrieved diff for PR #%d via CLI", prNumber) +} + +func TestCLIPRComment(t *testing.T) { + env := NewTestEnv(t) + + // PRs share the comment API with issues + issueNum := env.CreateTestIssue("[FGJ E2E Test] PR Comment Test", "Testing pr comment command") + defer env.CleanupIssue(issueNum) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "pr", "comment", + fmt.Sprintf("%d", issueNum), + "-b", "Automated test comment via fgj pr comment", + ) + + if result.ExitCode != 0 { + t.Fatalf("pr comment failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + t.Logf("Successfully commented on issue #%d via fgj pr comment", issueNum) +} + +// ===== CLI Repo Commands ===== + +func TestCLIRepoView(t *testing.T) { + env := NewTestEnv(t) + + result := env.RunCLI( + "--hostname", env.Hostname, + "repo", "view", + fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + ) + + if result.ExitCode != 0 { + t.Fatalf("repo view failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + if !strings.Contains(result.Stdout, env.RepoName) { + t.Fatalf("expected repo name in output, got: %s", result.Stdout) + } + + t.Logf("Successfully viewed repo via CLI") +} + +func TestCLIRepoList(t *testing.T) { + env := NewTestEnv(t) + + result := env.RunCLI( + "--hostname", env.Hostname, + "repo", "list", + ) + + if result.ExitCode != 0 { + t.Fatalf("repo list failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + if !strings.Contains(result.Stdout, env.RepoName) { + t.Fatalf("expected test repo in list output, got: %s", result.Stdout) + } + + t.Logf("Successfully listed repos via CLI") +} + +func TestCLIRepoCreate(t *testing.T) { + env := NewTestEnv(t) + + repoName := fmt.Sprintf("fgj-test-create-%d", time.Now().UnixNano()) + defer env.CleanupRepo(env.Owner, repoName) + + result := env.RunCLI( + "--hostname", env.Hostname, + "repo", "create", repoName, + "--public", + "-d", "Created by fgj functional test", + ) + + if result.ExitCode != 0 { + t.Fatalf("repo create failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + if !strings.Contains(result.Stdout, repoName) { + t.Fatalf("expected output to contain repo name %q, got: %s", repoName, result.Stdout) + } + + repo, _, err := env.Client.GetRepo(env.Owner, repoName) + if err != nil { + t.Fatalf("repo was not created or not accessible: %v", err) + } + if repo.Private { + t.Fatalf("expected public repo, got private") + } + if repo.Description != "Created by fgj functional test" { + t.Fatalf("expected description %q, got %q", "Created by fgj functional test", repo.Description) + } + + t.Logf("Successfully created repository %s via CLI", repo.FullName) +} + +func TestCLIRepoEdit(t *testing.T) { + env := NewTestEnv(t) + + repo, _, err := env.Client.GetRepo(env.Owner, env.RepoName) + if err != nil { + t.Fatalf("failed to get repo: %v", err) + } + originalDesc := repo.Description + + defer func() { + env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "repo", "edit", + "-d", originalDesc, + ) + }() + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "repo", "edit", + "-d", "Updated description via functional test", + ) + + if result.ExitCode != 0 { + t.Fatalf("repo edit failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + updatedRepo, _, err := env.Client.GetRepo(env.Owner, env.RepoName) + if err != nil { + t.Fatalf("failed to get repo after edit: %v", err) + } + + if updatedRepo.Description != "Updated description via functional test" { + t.Fatalf("expected description %q, got %q", + "Updated description via functional test", updatedRepo.Description) + } + + t.Logf("Successfully edited repo description via CLI") +} + func TestCLIRepoClone(t *testing.T) { env := NewTestEnv(t) - // For repo clone, we test by cloning the current repository (fgj itself) - // since that doesn't require special permissions tmpDir := t.TempDir() clonePath := fmt.Sprintf("%s/fgj-clone", tmpDir) - // Run: fgj repo clone romaintb/fgj - // Using the public fgj repository to avoid auth issues result := env.RunCLI( "--hostname", env.Hostname, "repo", "clone", - "romaintb/fgj", + fmt.Sprintf("%s/%s", env.Owner, env.RepoName), clonePath, ) if result.ExitCode != 0 { - t.Logf("Note: repo clone failed, which may be due to test environment limitations") - t.Logf("Exit code: %d", result.ExitCode) - t.Logf("Stderr: %s", result.Stderr) - // Skip instead of failing since this might be an auth/config issue in test env t.Skip("repo clone requires full authentication setup") } - // Verify the repository was cloned gitDir := fmt.Sprintf("%s/.git", clonePath) if _, err := os.Stat(gitDir); err != nil { t.Logf("Warning: .git directory not found, clone may not have completed") - t.Logf("Stdout: %s", result.Stdout) - // Don't fail - just note that clone didn't complete - // This might be expected in test environment return } t.Logf("Successfully cloned repository to %s via CLI", clonePath) } -// TestCLIReleaseCreateUploadDelete verifies release create/upload/delete via CLI. +// ===== CLI Release Commands ===== + +func TestCLIReleaseList(t *testing.T) { + env := NewTestEnv(t) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "release", "list", + ) + + if result.ExitCode != 0 { + t.Fatalf("release list failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + t.Logf("Successfully listed releases via CLI") +} + func TestCLIReleaseCreateUploadDelete(t *testing.T) { env := NewTestEnv(t) @@ -875,6 +811,7 @@ func TestCLIReleaseCreateUploadDelete(t *testing.T) { result := env.RunCLI( "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "release", "create", tag, "-t", title, "-n", notes, @@ -909,6 +846,7 @@ func TestCLIReleaseCreateUploadDelete(t *testing.T) { deleteResult := env.RunCLI( "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "release", "delete", tag, ) if deleteResult.ExitCode != 0 { @@ -921,61 +859,369 @@ func TestCLIReleaseCreateUploadDelete(t *testing.T) { } } -// TestCLIReleaseList verifies the `fgj release list` command works. -func TestCLIReleaseList(t *testing.T) { +func TestCLIReleaseView(t *testing.T) { + env := NewTestEnv(t) + + // Create a release to view + tag := fmt.Sprintf("fgj-view-test-%d", time.Now().UnixNano()) + + createResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "release", "create", tag, + "-t", "View Test Release", + "-n", "For release view test", + ) + if createResult.ExitCode != 0 { + t.Fatalf("release create failed: %s", createResult.Stderr) + } + + defer env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "release", "delete", tag, + ) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "release", "view", tag, + ) + + if result.ExitCode != 0 { + t.Fatalf("release view failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + if !strings.Contains(result.Stdout, "View Test Release") { + t.Fatalf("expected release title in output, got: %s", result.Stdout) + } + + t.Logf("Successfully viewed release %s via CLI", tag) +} + +// ===== CLI Label Commands ===== + +func TestCLILabelCreateListEditDelete(t *testing.T) { + env := NewTestEnv(t) + + rf := fmt.Sprintf("%s/%s", env.Owner, env.RepoName) + + createResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "label", "create", "test-label", + "--color", "00ff00", + "-d", "Test label for functional tests", + ) + + if createResult.ExitCode != 0 { + t.Fatalf("label create failed with exit code %d: %s", createResult.ExitCode, createResult.Stderr) + } + + listResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "label", "list", + ) + + if listResult.ExitCode != 0 { + t.Fatalf("label list failed with exit code %d: %s", listResult.ExitCode, listResult.Stderr) + } + + if !strings.Contains(listResult.Stdout, "test-label") { + t.Fatalf("label list output does not contain 'test-label': %s", listResult.Stdout) + } + + editResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "label", "edit", "test-label", + "--name", "test-label-renamed", + ) + + if editResult.ExitCode != 0 { + t.Fatalf("label edit failed with exit code %d: %s", editResult.ExitCode, editResult.Stderr) + } + + deleteResult := env.runCLIWithStdin("y\n", + "--hostname", env.Hostname, + "-R", rf, + "label", "delete", "test-label-renamed", + ) + + if deleteResult.ExitCode != 0 { + t.Fatalf("label delete failed with exit code %d: %s", deleteResult.ExitCode, deleteResult.Stderr) + } + + t.Logf("Successfully created, listed, edited, and deleted label via CLI") +} + +// ===== CLI Milestone Commands ===== + +func TestCLIMilestoneCreateListViewEditDelete(t *testing.T) { + env := NewTestEnv(t) + + rf := fmt.Sprintf("%s/%s", env.Owner, env.RepoName) + + createResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "milestone", "create", "Test Milestone", + "-d", "For testing", + ) + + if createResult.ExitCode != 0 { + t.Fatalf("milestone create failed with exit code %d: %s", createResult.ExitCode, createResult.Stderr) + } + + listResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "milestone", "list", + ) + + if listResult.ExitCode != 0 { + t.Fatalf("milestone list failed with exit code %d: %s", listResult.ExitCode, listResult.Stderr) + } + + if !strings.Contains(listResult.Stdout, "Test Milestone") { + t.Fatalf("milestone list output does not contain 'Test Milestone': %s", listResult.Stdout) + } + + viewResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "milestone", "view", "Test Milestone", + ) + + if viewResult.ExitCode != 0 { + t.Fatalf("milestone view failed with exit code %d: %s", viewResult.ExitCode, viewResult.Stderr) + } + + editResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "milestone", "edit", "Test Milestone", + "--title", "Updated Milestone", + ) + + if editResult.ExitCode != 0 { + t.Fatalf("milestone edit failed with exit code %d: %s", editResult.ExitCode, editResult.Stderr) + } + + deleteResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "milestone", "delete", "Updated Milestone", + ) + + if deleteResult.ExitCode != 0 { + t.Fatalf("milestone delete failed with exit code %d: %s", deleteResult.ExitCode, deleteResult.Stderr) + } + + t.Logf("Successfully created, listed, viewed, edited, and deleted milestone via CLI") +} + +// ===== CLI Wiki Commands ===== + +func TestCLIWikiCreateListViewEditDelete(t *testing.T) { + env := NewTestEnv(t) + + rf := fmt.Sprintf("%s/%s", env.Owner, env.RepoName) + + createResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "wiki", "create", "Test Page", + "-b", "# Test\nContent", + ) + + if createResult.ExitCode != 0 { + t.Fatalf("wiki create failed with exit code %d: %s", createResult.ExitCode, createResult.Stderr) + } + + defer func() { + env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "wiki", "delete", "Test Page", + ) + }() + + listResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "wiki", "list", + ) + + if listResult.ExitCode != 0 { + t.Fatalf("wiki list failed with exit code %d: %s", listResult.ExitCode, listResult.Stderr) + } + + if !strings.Contains(listResult.Stdout, "Test Page") { + t.Fatalf("wiki list output does not contain 'Test Page': %s", listResult.Stdout) + } + + viewResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "wiki", "view", "Test Page", + ) + + if viewResult.ExitCode != 0 { + t.Fatalf("wiki view failed with exit code %d: %s", viewResult.ExitCode, viewResult.Stderr) + } + + if !strings.Contains(viewResult.Stdout, "Content") { + t.Fatalf("wiki view output does not contain 'Content': %s", viewResult.Stdout) + } + + editResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "wiki", "edit", "Test Page", + "-b", "Updated content", + ) + + if editResult.ExitCode != 0 { + t.Fatalf("wiki edit failed with exit code %d: %s", editResult.ExitCode, editResult.Stderr) + } + + deleteResult := env.RunCLI( + "--hostname", env.Hostname, + "-R", rf, + "wiki", "delete", "Test Page", + ) + + if deleteResult.ExitCode != 0 { + t.Fatalf("wiki delete failed with exit code %d: %s", deleteResult.ExitCode, deleteResult.Stderr) + } + + t.Logf("Successfully created, listed, viewed, edited, and deleted wiki page via CLI") +} + +// ===== CLI Actions Commands ===== + +func TestCLIActionsRunList(t *testing.T) { env := NewTestEnv(t) result := env.RunCLI( "--hostname", env.Hostname, - "release", "list", + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "actions", "run", "list", ) + // Actions might not be enabled, so we accept both success and failure if result.ExitCode != 0 { - t.Fatalf("release list failed with exit code %d: %s", result.ExitCode, result.Stderr) + if bytes.Contains([]byte(result.Stderr), []byte("404")) { + t.Logf("Note: Actions not available on this instance") + return + } + t.Fatalf("actions run list failed with exit code %d: %s", result.ExitCode, result.Stderr) } - if result.Stdout == "" && result.Stderr == "" { - t.Logf("Note: release list produced no output") - } - - t.Logf("Successfully listed releases via CLI") + t.Logf("Successfully listed workflow runs via CLI") } -// TestCLIRepoCreate verifies `fgj repo create` creates a repository -func TestCLIRepoCreate(t *testing.T) { +// ===== CLI API Command ===== + +func TestCLIAPIGet(t *testing.T) { env := NewTestEnv(t) - repoName := fmt.Sprintf("fgj-test-create-%d", time.Now().UnixNano()) - defer env.CleanupRepo(env.Owner, repoName) + endpoint := fmt.Sprintf("/repos/%s/%s", env.Owner, env.RepoName) result := env.RunCLI( "--hostname", env.Hostname, - "repo", "create", repoName, - "--public", - "-d", "Created by fgj functional test", + "api", endpoint, ) if result.ExitCode != 0 { - t.Fatalf("repo create failed with exit code %d: %s", result.ExitCode, result.Stderr) + t.Fatalf("api GET failed with exit code %d: %s", result.ExitCode, result.Stderr) } - if !bytes.Contains([]byte(result.Stdout), []byte(repoName)) { - t.Fatalf("expected output to contain repo name %q, got: %s", repoName, result.Stdout) + var data map[string]interface{} + if err := json.Unmarshal([]byte(result.Stdout), &data); err != nil { + t.Fatalf("failed to parse JSON output: %v", err) } - repo, _, err := env.Client.GetRepo(env.Owner, repoName) - if err != nil { - t.Fatalf("repo was not created or not accessible: %v", err) - } - if repo.Name != repoName { - t.Fatalf("expected repo name %q, got %q", repoName, repo.Name) - } - if repo.Private { - t.Fatalf("expected public repo, got private") - } - if repo.Description != "Created by fgj functional test" { - t.Fatalf("expected description %q, got %q", "Created by fgj functional test", repo.Description) + name, ok := data["name"].(string) + if !ok || name != env.RepoName { + t.Fatalf("expected repo name %q in JSON output, got %v", env.RepoName, data["name"]) } - t.Logf("Successfully created repository %s via CLI", repo.FullName) + t.Logf("Successfully retrieved repo info via fgj api GET") +} + +func TestCLIAPIPostAndDelete(t *testing.T) { + env := NewTestEnv(t) + + endpoint := fmt.Sprintf("/repos/%s/%s/issues", env.Owner, env.RepoName) + + result := env.RunCLI( + "--hostname", env.Hostname, + "api", endpoint, + "-X", "POST", + "-f", "title=[FGJ E2E Test] API Post Test", + "-f", "body=Created via fgj api command", + ) + + if result.ExitCode != 0 { + t.Fatalf("api POST failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + var issueData map[string]interface{} + if err := json.Unmarshal([]byte(result.Stdout), &issueData); err != nil { + t.Fatalf("failed to parse JSON response: %v", err) + } + + issueNumber, ok := issueData["number"].(float64) + if !ok || issueNumber == 0 { + t.Fatalf("expected issue number in response, got %v", issueData["number"]) + } + + issueNum := int64(issueNumber) + defer env.CleanupIssue(issueNum) + + t.Logf("Successfully created issue #%d via fgj api POST", issueNum) +} + +// ===== Structured Error Output ===== + +func TestCLIJSONErrors(t *testing.T) { + env := NewTestEnv(t) + + result := env.RunCLI( + "--hostname", env.Hostname, + "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), + "issue", "view", "999999", + "--json-errors", + ) + + if result.ExitCode == 0 { + t.Fatalf("expected non-zero exit code for non-existent issue, got 0") + } + + // Stderr may contain cobra usage text mixed with JSON error output. + stderr := result.Stderr + jsonStart := strings.Index(stderr, "{") + jsonEnd := strings.LastIndex(stderr, "}") + if jsonStart < 0 || jsonEnd < 0 || jsonEnd <= jsonStart { + t.Fatalf("expected JSON error output in stderr, but no JSON found.\nStderr: %s", stderr) + } + jsonStr := stderr[jsonStart : jsonEnd+1] + + var errData map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &errData); err != nil { + t.Fatalf("expected JSON error output in stderr, got parse error: %v\nJSON: %s\nFull stderr: %s", err, jsonStr, stderr) + } + + if _, hasCode := errData["code"]; !hasCode { + if _, hasMsg := errData["message"]; !hasMsg { + if _, hasErr := errData["error"]; !hasErr { + t.Fatalf("JSON error output missing expected fields (code/message/error): %v", errData) + } + } + } + + t.Logf("Successfully verified --json-errors flag produces structured JSON error output") }