//go:build functional // +build functional package functional import ( "bytes" "encoding/json" "fmt" "os" "strings" "testing" "time" "code.gitea.io/sdk/gitea" ) // ===== SDK API tests (verify test environment and API connectivity) ===== func TestAPIConnection(t *testing.T) { env := NewTestEnv(t) env.VerifyAPIConnection() } func TestListRepositories(t *testing.T) { env := NewTestEnv(t) repos, _, err := env.Client.ListUserRepos(env.Owner, gitea.ListReposOptions{}) if err != nil { t.Fatalf("failed to list repositories: %v", err) } if len(repos) == 0 { t.Fatalf("expected at least one repository, got none") } t.Logf("Found %d repositories", len(repos)) found := false for _, repo := range repos { if repo.Name == env.RepoName { found = true t.Logf("Found test repository: %s", repo.FullName) break } } if !found { t.Fatalf("test repository %s not found in user's repositories", env.RepoName) } } func TestGetRepository(t *testing.T) { env := NewTestEnv(t) repo, _, err := env.Client.GetRepo(env.Owner, env.RepoName) if err != nil { t.Fatalf("failed to get repository: %v", err) } if repo.Name != env.RepoName { t.Fatalf("expected repo name %s, got %s", env.RepoName, repo.Name) } if repo.Owner.UserName != env.Owner { t.Fatalf("expected owner %s, got %s", env.Owner, repo.Owner.UserName) } t.Logf("Repository: %s (%s)", repo.FullName, repo.Description) } func TestAPIErrorHandling(t *testing.T) { env := NewTestEnv(t) _, _, 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) } // ===== 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 issueNum == 0 { t.Fatalf("failed to parse issue number from output: %s", result.Stdout) } defer env.CleanupIssue(issueNum) t.Logf("Successfully created issue #%d via CLI", issueNum) } func TestCLIIssueCreateWithLabels(t *testing.T) { env := NewTestEnv(t) env.EnsureTestLabels() 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", "-l", "bug", "-l", "enhancement", ) if result.ExitCode != 0 { t.Fatalf("issue create with labels 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 issueNum == 0 { t.Fatalf("failed to parse issue number from output: %s", result.Stdout) } defer env.CleanupIssue(issueNum) labels, err := env.GetIssueLabels(issueNum) if err != nil { t.Fatalf("failed to get issue labels: %v", err) } if len(labels) != 2 { t.Fatalf("expected 2 labels, got %d", len(labels)) } labelNames := make(map[string]bool) for _, label := range labels { labelNames[label.Name] = true } if !labelNames["bug"] || !labelNames["enhancement"] { t.Fatalf("expected labels 'bug' and 'enhancement', got %v", labelNames) } t.Logf("Successfully created issue #%d with labels: bug, enhancement", issueNum) } func TestCLIIssueComment(t *testing.T) { env := NewTestEnv(t) issueNum := env.CreateTestIssue("[FGJ E2E Test] Comment Test", "Testing comment via CLI") defer env.CleanupIssue(issueNum) 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", "--add-label", "help-wanted", ) if result.ExitCode != 0 { t.Fatalf("issue edit add-label failed with exit code %d: %s", result.ExitCode, result.Stderr) } labels, err := env.GetIssueLabels(issueNum) if err != nil { t.Fatalf("failed to get labels after edit: %v", err) } if len(labels) != 2 { t.Fatalf("expected 2 labels after edit, got %d", len(labels)) } labelNames := make(map[string]bool) for _, label := range labels { labelNames[label.Name] = true } if !labelNames["bug"] || !labelNames["help-wanted"] { t.Fatalf("expected labels 'bug' and 'help-wanted', got %v", labelNames) } t.Logf("Successfully added labels to issue #%d via CLI", issueNum) } func TestCLIIssueEditRemoveLabels(t *testing.T) { env := NewTestEnv(t) env.EnsureTestLabels() issue, _, err := env.Client.CreateIssue(env.Owner, env.RepoName, gitea.CreateIssueOption{ 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) labelIDs := env.GetLabelIDs([]string{"bug", "enhancement"}) _, _, err = env.Client.AddIssueLabels(env.Owner, env.RepoName, issue.Index, gitea.IssueLabelsOption{ Labels: labelIDs, }) if err != nil { t.Fatalf("failed to add initial labels: %v", err) } 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", ) if result.ExitCode != 0 { t.Fatalf("issue edit remove-label failed with exit code %d: %s", result.ExitCode, result.Stderr) } labels, err := env.GetIssueLabels(issue.Index) if err != nil { t.Fatalf("failed to get labels after edit: %v", err) } if len(labels) != 1 { t.Fatalf("expected 1 label after removal, got %d", len(labels)) } if labels[0].Name != "enhancement" { t.Fatalf("expected remaining label 'enhancement', got '%s'", labels[0].Name) } t.Logf("Successfully removed label from issue #%d via CLI", issue.Index) } // ===== CLI PR Commands ===== func TestCLIPRList(t *testing.T) { env := NewTestEnv(t) result := env.RunCLI( "--hostname", env.Hostname, "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "pr", "list", ) if result.ExitCode != 0 { t.Fatalf("pr list failed with exit code %d: %s", result.ExitCode, result.Stderr) } t.Logf("Successfully listed pull requests via CLI") } func TestCLIPRListJSON(t *testing.T) { env := NewTestEnv(t) result := env.RunCLI( "--hostname", env.Hostname, "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "pr", "list", "--json", ) if result.ExitCode != 0 { t.Fatalf("pr list --json failed with exit code %d: %s", result.ExitCode, result.Stderr) } 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 %d PRs via CLI with --json", len(prs)) } func TestCLIPRView(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 to view") } prNumber := prs[0].Index result := env.RunCLI( "--hostname", env.Hostname, "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "pr", "view", fmt.Sprintf("%d", prNumber), ) if result.ExitCode != 0 { t.Fatalf("pr view failed with exit code %d: %s", result.ExitCode, result.Stderr) } if result.Stdout == "" { t.Fatalf("pr view produced no output") } t.Logf("Successfully viewed PR #%d via CLI", prNumber) } 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) tmpDir := t.TempDir() clonePath := fmt.Sprintf("%s/fgj-clone", tmpDir) result := env.RunCLI( "--hostname", env.Hostname, "repo", "clone", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), clonePath, ) if result.ExitCode != 0 { t.Skip("repo clone requires full authentication setup") } 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") return } t.Logf("Successfully cloned repository to %s via CLI", clonePath) } // ===== 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) tag := fmt.Sprintf("fgj-test-%d", time.Now().UnixNano()) title := "FGJ CLI Release Test" notes := "Release created by functional tests" tmpDir := t.TempDir() assetPath := fmt.Sprintf("%s/asset.txt", tmpDir) if err := os.WriteFile(assetPath, []byte("fgj release asset"), 0600); err != nil { t.Fatalf("failed to create asset file: %v", err) } result := env.RunCLI( "--hostname", env.Hostname, "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "release", "create", tag, "-t", title, "-n", notes, assetPath, ) if result.ExitCode != 0 { t.Fatalf("release create failed with exit code %d: %s", result.ExitCode, result.Stderr) } release, _, err := env.Client.GetReleaseByTag(env.Owner, env.RepoName, tag) if err != nil { t.Fatalf("failed to fetch created release: %v", err) } if release.Title != title { t.Fatalf("expected release title %q, got %q", title, release.Title) } attachments, _, err := env.Client.ListReleaseAttachments(env.Owner, env.RepoName, release.ID, gitea.ListReleaseAttachmentsOptions{}) if err != nil { t.Fatalf("failed to list release assets: %v", err) } found := false for _, attachment := range attachments { if attachment.Name == "asset.txt" { found = true break } } if !found { t.Fatalf("uploaded asset not found in release") } deleteResult := env.RunCLI( "--hostname", env.Hostname, "-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName), "release", "delete", tag, ) if deleteResult.ExitCode != 0 { t.Fatalf("release delete failed with exit code %d: %s", deleteResult.ExitCode, deleteResult.Stderr) } _, _, err = env.Client.GetReleaseByTag(env.Owner, env.RepoName, tag) if err == nil { t.Fatalf("expected release %s to be deleted, but it still exists", tag) } } 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, "-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 { 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) } t.Logf("Successfully listed workflow runs via CLI") } // ===== CLI API Command ===== func TestCLIAPIGet(t *testing.T) { env := NewTestEnv(t) endpoint := fmt.Sprintf("/repos/%s/%s", env.Owner, env.RepoName) result := env.RunCLI( "--hostname", env.Hostname, "api", endpoint, ) if result.ExitCode != 0 { t.Fatalf("api GET 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", err) } 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 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") }