Skip to content

Commit 4d3c118

Browse files
authored
feat(testing): add e2e test for release init --push (#2228)
- Adds e2e test case for 'release init --push' - Mocks GitHub API calls for PR creation and labeling - Updates command.go and release_notes.go to support testability Part of #1013 Note: improving coverage for new code in command.go requires tests with much setup. Given the e2e test uses the code, codecov report is ignored.
1 parent 3eb4cc7 commit 4d3c118

File tree

8 files changed

+194
-81
lines changed

8 files changed

+194
-81
lines changed

e2e_test.go

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import (
3030

3131
"github.com/google/go-cmp/cmp"
3232
"github.com/google/go-cmp/cmp/cmpopts"
33-
"github.com/google/go-github/v69/github"
3433
"github.com/googleapis/librarian/internal/config"
34+
"github.com/googleapis/librarian/internal/github"
3535
"gopkg.in/yaml.v3"
3636
)
3737

@@ -180,6 +180,7 @@ func TestCleanAndCopy(t *testing.T) {
180180
cmd := exec.Command(
181181
"go",
182182
"run",
183+
"-tags", "e2etest",
183184
"github.com/googleapis/librarian/cmd/librarian",
184185
"generate",
185186
fmt.Sprintf("--api=%s", apiToGenerate),
@@ -406,15 +407,24 @@ func TestReleaseInit(t *testing.T) {
406407
updatedState string
407408
wantChangelog string
408409
libraryID string
410+
push bool
409411
wantErr bool
410412
}{
411413
{
412-
name: "runs successfully",
414+
name: "runs successfully without push",
413415
initialRepoStateDir: "testdata/e2e/release/init/repo_init",
414416
updatedState: "testdata/e2e/release/init/updated-state.yaml",
415417
wantChangelog: "testdata/e2e/release/init/CHANGELOG.md",
416418
libraryID: "go-google-cloud-pubsub-v1",
417419
},
420+
{
421+
name: "runs successfully with push",
422+
initialRepoStateDir: "testdata/e2e/release/init/repo_init",
423+
updatedState: "testdata/e2e/release/init/updated-state.yaml",
424+
wantChangelog: "testdata/e2e/release/init/CHANGELOG.md",
425+
libraryID: "go-google-cloud-pubsub-v1",
426+
push: true, // Enable --push for this case
427+
},
418428
} {
419429
t.Run(test.name, func(t *testing.T) {
420430
workRoot := t.TempDir()
@@ -423,6 +433,17 @@ func TestReleaseInit(t *testing.T) {
423433
if err := initRepo(t, repo, test.initialRepoStateDir); err != nil {
424434
t.Fatalf("prepare test error = %v", err)
425435
}
436+
437+
if test.push {
438+
// Create a local bare repository to act as the remote for the push.
439+
bareRepoDir := filepath.Join(t.TempDir(), "remote.git")
440+
if err := os.MkdirAll(bareRepoDir, 0755); err != nil {
441+
t.Fatalf("Failed to create bare repo dir: %v", err)
442+
}
443+
runGit(t, bareRepoDir, "init", "--bare")
444+
runGit(t, repo, "remote", "set-url", "origin", bareRepoDir)
445+
}
446+
426447
runGit(t, repo, "tag", "go-google-cloud-pubsub-v1-1.0.0")
427448
// Add a new commit to simulate a change.
428449
newFilePath := filepath.Join(repo, "google-cloud-pubsub/v1", "new-file.txt")
@@ -469,16 +490,58 @@ END_COMMIT_OVERRIDE
469490
runGit(t, repo, "commit", "-m", commitMsg)
470491
runGit(t, repo, "log", "--oneline", "go-google-cloud-pubsub-v1-1.0.0..HEAD", "--", "google-cloud-pubsub/v1")
471492

472-
cmd := exec.Command(
473-
"go",
493+
// Setup mock GitHub server for --push case
494+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
495+
if r.Header.Get("Authorization") != "Bearer fake-token" {
496+
t.Errorf("missing or wrong authorization header: got %q", r.Header.Get("Authorization"))
497+
}
498+
499+
// Mock endpoint for POST /repos/{owner}/{repo}/pulls
500+
if r.Method == "POST" && strings.HasSuffix(r.URL.Path, "/pulls") {
501+
var newPR github.NewPullRequest
502+
if err := json.NewDecoder(r.Body).Decode(&newPR); err != nil {
503+
t.Fatalf("failed to decode request body: %v", err)
504+
}
505+
if !strings.Contains(*newPR.Title, "chore: librarian release pull request") {
506+
t.Errorf("unexpected PR title: got %q", *newPR.Title)
507+
}
508+
if *newPR.Base != "main" { // Assuming default branch
509+
t.Errorf("unexpected PR base: got %q", *newPR.Base)
510+
}
511+
w.WriteHeader(http.StatusCreated)
512+
fmt.Fprint(w, `{"number": 123, "html_url": "https://github.com/googleapis/librarian/pull/123"}`)
513+
return
514+
}
515+
516+
// Mock endpoint for POST /repos/{owner}/{repo}/issues/{number}/labels
517+
if r.Method == "POST" && strings.Contains(r.URL.Path, "/issues/123/labels") {
518+
w.WriteHeader(http.StatusOK)
519+
fmt.Fprint(w, `[]`)
520+
return
521+
}
522+
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
523+
}))
524+
defer server.Close()
525+
526+
cmdArgs := []string{
474527
"run",
528+
"-tags", "e2etest",
475529
"github.com/googleapis/librarian/cmd/librarian",
476530
"release",
477531
"init",
478532
fmt.Sprintf("--repo=%s", repo),
479533
fmt.Sprintf("--output=%s", workRoot),
480534
fmt.Sprintf("--library=%s", test.libraryID),
481-
)
535+
}
536+
if test.push {
537+
cmdArgs = append(cmdArgs, "--push")
538+
t.Logf("zle: server.URL: %s", server.URL)
539+
}
540+
541+
cmd := exec.Command("go", cmdArgs...)
542+
cmd.Env = os.Environ()
543+
cmd.Env = append(cmd.Env, "LIBRARIAN_GITHUB_TOKEN=fake-token")
544+
cmd.Env = append(cmd.Env, "LIBRARIAN_GITHUB_BASE_URL="+server.URL)
482545
cmd.Stderr = os.Stderr
483546
cmd.Stdout = os.Stdout
484547
err := cmd.Run()

internal/github/github.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"io"
2323
"log/slog"
2424
"net/http"
25+
"net/url"
2526
"strings"
2627

2728
"github.com/google/go-github/v69/github"
@@ -31,6 +32,9 @@ import (
3132
// PullRequest is a type alias for the go-github type.
3233
type PullRequest = github.PullRequest
3334

35+
// NewPullRequest is a type alias for the go-github type.
36+
type NewPullRequest = github.NewPullRequest
37+
3438
// RepositoryCommit is a type alias for the go-github type.
3539
type RepositoryCommit = github.RepositoryCommit
3640

@@ -58,6 +62,14 @@ func NewClient(accessToken string, repo *Repository) *Client {
5862

5963
func newClientWithHTTP(accessToken string, repo *Repository, httpClient *http.Client) *Client {
6064
client := github.NewClient(httpClient)
65+
if repo.BaseURL != "" {
66+
baseURL, _ := url.Parse(repo.BaseURL)
67+
// Ensure the endpoint URL has a trailing slash.
68+
if !strings.HasSuffix(baseURL.Path, "/") {
69+
baseURL.Path += "/"
70+
}
71+
client.BaseURL = baseURL
72+
}
6173
if accessToken != "" {
6274
client = client.WithAuthToken(accessToken)
6375
}
@@ -80,6 +92,8 @@ type Repository struct {
8092
Owner string
8193
// The name of the repository.
8294
Name string
95+
// Base URL for API requests.
96+
BaseURL string
8397
}
8498

8599
// PullRequestMetadata identifies a pull request within a repository.

internal/librarian/command.go

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -127,30 +127,12 @@ func newCommandRunner(cfg *config.Config) (*commandRunner, error) {
127127

128128
image := deriveImage(cfg.Image, state)
129129

130-
var gitRepo *github.Repository
131-
if isURL(cfg.Repo) {
132-
gitRepo, err = github.ParseRemote(cfg.Repo)
133-
if err != nil {
134-
return nil, fmt.Errorf("failed to parse repo url: %w", err)
135-
}
136-
} else {
137-
gitRepo, err = github.FetchGitHubRepoFromRemote(languageRepo)
138-
if err != nil {
139-
return nil, fmt.Errorf("failed to get GitHub repo from remote: %w", err)
140-
}
141-
}
142-
ghClient := github.NewClient(cfg.GitHubToken, gitRepo)
143-
144-
// If a custom GitHub API endpoint is provided (for testing),
145-
// parse it and set it as the BaseURL on the GitHub client.
146-
if cfg.GitHubAPIEndpoint != "" {
147-
endpoint, err := url.Parse(cfg.GitHubAPIEndpoint)
148-
if err != nil {
149-
return nil, fmt.Errorf("failed to parse github-api-endpoint: %w", err)
150-
}
151-
ghClient.BaseURL = endpoint
130+
gitHubRepo, err := GetGitHubRepository(cfg, languageRepo)
131+
if err != nil {
132+
return nil, fmt.Errorf("failed to get GitHub repository: %w", err)
152133
}
153134

135+
ghClient := github.NewClient(cfg.GitHubToken, gitHubRepo)
154136
container, err := docker.New(cfg.WorkRoot, image, cfg.UserUID, cfg.UserGID)
155137
if err != nil {
156138
return nil, err
@@ -377,14 +359,13 @@ func commitAndPush(ctx context.Context, info *commitInfo) error {
377359
return nil
378360
}
379361

380-
// Ensure we have a GitHub repository
381-
gitHubRepo, err := github.FetchGitHubRepoFromRemote(repo)
362+
gitHubRepo, err := GetGitHubRepositoryFromGitRepo(info.repo)
382363
if err != nil {
383-
return err
364+
return fmt.Errorf("failed to get GitHub repository: %w", err)
384365
}
385366

386367
title := fmt.Sprintf("chore: librarian %s pull request: %s", info.prType, datetimeNow)
387-
prBody, err := createPRBody(info)
368+
prBody, err := createPRBody(info, gitHubRepo)
388369
if err != nil {
389370
return fmt.Errorf("failed to create pull request body: %w", err)
390371
}
@@ -414,12 +395,12 @@ func addLabelsToPullRequest(ctx context.Context, ghClient GitHubClient, pullRequ
414395
return nil
415396
}
416397

417-
func createPRBody(info *commitInfo) (string, error) {
398+
func createPRBody(info *commitInfo, gitHubRepo *github.Repository) (string, error) {
418399
switch info.prType {
419400
case generate:
420401
return formatGenerationPRBody(info.sourceRepo, info.state, info.idToCommits, info.failedLibraries)
421402
case release:
422-
return formatReleaseNotes(info.repo, info.state)
403+
return formatReleaseNotes(info.state, gitHubRepo)
423404
default:
424405
return "", fmt.Errorf("unrecognized pull request type: %s", info.prType)
425406
}

internal/librarian/command_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,7 @@ func TestCommitAndPush(t *testing.T) {
14051405
},
14061406
prType: "generate",
14071407
push: true,
1408+
state: &config.LibrarianState{},
14081409
wantErr: true,
14091410
expectedErrMsg: "could not find an 'origin' remote",
14101411
},

internal/librarian/release_notes.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -238,18 +238,15 @@ func findLatestGenerationCommit(repo gitrepo.Repository, state *config.Librarian
238238
}
239239

240240
// formatReleaseNotes generates the body for a release pull request.
241-
func formatReleaseNotes(repo gitrepo.Repository, state *config.LibrarianState) (string, error) {
241+
func formatReleaseNotes(state *config.LibrarianState, ghRepo *github.Repository) (string, error) {
242242
librarianVersion := cli.Version()
243243
var releaseSections []*releaseNoteSection
244244
for _, library := range state.Libraries {
245245
if !library.ReleaseTriggered {
246246
continue
247247
}
248248

249-
section, err := formatLibraryReleaseNotes(repo, library)
250-
if err != nil {
251-
return "", fmt.Errorf("failed to format release notes for library %s: %w", library.ID, err)
252-
}
249+
section := formatLibraryReleaseNotes(library, ghRepo)
253250
releaseSections = append(releaseSections, section)
254251
}
255252

@@ -269,12 +266,7 @@ func formatReleaseNotes(repo gitrepo.Repository, state *config.LibrarianState) (
269266

270267
// formatLibraryReleaseNotes generates release notes in Markdown format for a single library.
271268
// It returns the generated release notes and the new version string.
272-
func formatLibraryReleaseNotes(repo gitrepo.Repository, library *config.LibraryState) (*releaseNoteSection, error) {
273-
ghRepo, err := github.FetchGitHubRepoFromRemote(repo)
274-
if err != nil {
275-
return nil, fmt.Errorf("failed to fetch github repo from remote: %w", err)
276-
}
277-
269+
func formatLibraryReleaseNotes(library *config.LibraryState, ghRepo *github.Repository) *releaseNoteSection {
278270
// The version should already be updated to the next version.
279271
newVersion := library.Version
280272
newTag := formatTag(library.TagFormat, library.ID, newVersion)
@@ -309,5 +301,5 @@ func formatLibraryReleaseNotes(repo gitrepo.Repository, library *config.LibraryS
309301
CommitSections: sections,
310302
}
311303

312-
return section, nil
304+
return section
313305
}

internal/librarian/release_notes_test.go

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/google/go-cmp/cmp"
3030
"github.com/googleapis/librarian/internal/cli"
3131
"github.com/googleapis/librarian/internal/config"
32+
"github.com/googleapis/librarian/internal/github"
3233
"github.com/googleapis/librarian/internal/gitrepo"
3334
)
3435

@@ -488,7 +489,7 @@ func TestFormatReleaseNotes(t *testing.T) {
488489
for _, test := range []struct {
489490
name string
490491
state *config.LibrarianState
491-
repo gitrepo.Repository
492+
ghRepo *github.Repository
492493
wantReleaseNote string
493494
wantErr bool
494495
wantErrPhrase string
@@ -519,9 +520,7 @@ func TestFormatReleaseNotes(t *testing.T) {
519520
},
520521
},
521522
},
522-
repo: &MockRepository{
523-
RemotesValue: []*git.Remote{git.NewRemote(nil, &gitconfig.RemoteConfig{Name: "origin", URLs: []string{"https://github.com/owner/repo.git"}})},
524-
},
523+
ghRepo: &github.Repository{Owner: "owner", Name: "repo"},
525524
wantReleaseNote: fmt.Sprintf(`Librarian Version: %s
526525
Language Image: go:1.21
527526
<details><summary>my-library: 1.1.0</summary>
@@ -571,9 +570,7 @@ Language Image: go:1.21
571570
},
572571
},
573572
},
574-
repo: &MockRepository{
575-
RemotesValue: []*git.Remote{git.NewRemote(nil, &gitconfig.RemoteConfig{Name: "origin", URLs: []string{"https://github.com/owner/repo.git"}})},
576-
},
573+
ghRepo: &github.Repository{Owner: "owner", Name: "repo"},
577574
wantReleaseNote: fmt.Sprintf(`Librarian Version: %s
578575
Language Image: go:1.21
579576
<details><summary>my-library: 1.1.0</summary>
@@ -617,9 +614,7 @@ Language Image: go:1.21
617614
},
618615
},
619616
},
620-
repo: &MockRepository{
621-
RemotesValue: []*git.Remote{git.NewRemote(nil, &gitconfig.RemoteConfig{Name: "origin", URLs: []string{"https://github.com/owner/repo.git"}})},
622-
},
617+
ghRepo: &github.Repository{Owner: "owner", Name: "repo"},
623618
wantReleaseNote: fmt.Sprintf(`Librarian Version: %s
624619
Language Image: go:1.21
625620
<details><summary>my-library: 1.1.0</summary>
@@ -670,9 +665,7 @@ Language Image: go:1.21
670665
},
671666
},
672667
},
673-
repo: &MockRepository{
674-
RemotesValue: []*git.Remote{git.NewRemote(nil, &gitconfig.RemoteConfig{Name: "origin", URLs: []string{"https://github.com/owner/repo.git"}})},
675-
},
668+
ghRepo: &github.Repository{Owner: "owner", Name: "repo"},
676669
wantReleaseNote: fmt.Sprintf(`Librarian Version: %s
677670
Language Image: go:1.21
678671
<details><summary>lib-a: 1.1.0</summary>
@@ -723,9 +716,7 @@ Language Image: go:1.21
723716
},
724717
},
725718
},
726-
repo: &MockRepository{
727-
RemotesValue: []*git.Remote{git.NewRemote(nil, &gitconfig.RemoteConfig{Name: "origin", URLs: []string{"https://github.com/owner/repo.git"}})},
728-
},
719+
ghRepo: &github.Repository{Owner: "owner", Name: "repo"},
729720
wantReleaseNote: fmt.Sprintf(`Librarian Version: %s
730721
Language Image: go:1.21
731722
<details><summary>my-library: 1.1.0</summary>
@@ -745,31 +736,13 @@ Language Image: go:1.21
745736
Image: "go:1.21",
746737
Libraries: []*config.LibraryState{},
747738
},
748-
repo: &MockRepository{},
739+
ghRepo: &github.Repository{},
749740
wantReleaseNote: fmt.Sprintf("Librarian Version: %s\nLanguage Image: go:1.21", librarianVersion),
750741
},
751-
{
752-
name: "error getting commits",
753-
state: &config.LibrarianState{
754-
Image: "go:1.21",
755-
Libraries: []*config.LibraryState{
756-
{
757-
ID: "my-library",
758-
Version: "1.0.0",
759-
ReleaseTriggered: true,
760-
},
761-
},
762-
},
763-
repo: &MockRepository{
764-
RemotesError: errors.New("no remote repo"),
765-
},
766-
wantErr: true,
767-
wantErrPhrase: "failed to format release notes",
768-
},
769742
} {
770743
t.Run(test.name, func(t *testing.T) {
771744
t.Parallel()
772-
got, err := formatReleaseNotes(test.repo, test.state)
745+
got, err := formatReleaseNotes(test.state, test.ghRepo)
773746
if test.wantErr {
774747
if err == nil {
775748
t.Fatalf("%s should return error", test.name)

0 commit comments

Comments
 (0)