@@ -16,14 +16,19 @@ package github
1616
1717import (
1818 "context"
19+ "encoding/json"
1920 "fmt"
2021 "net/http"
2122 "net/http/httptest"
2223 "net/url"
2324 "strings"
2425 "testing"
2526
27+ "github.com/go-git/go-git/v5"
28+ gogitConfig "github.com/go-git/go-git/v5/config"
2629 "github.com/google/go-cmp/cmp"
30+ "github.com/google/go-github/v69/github"
31+ "github.com/googleapis/librarian/internal/gitrepo"
2732)
2833
2934func TestToken (t * testing.T ) {
@@ -122,6 +127,120 @@ func TestGetRawContent(t *testing.T) {
122127 }
123128}
124129
130+ // newTestGitRepo creates a new git repository in a temporary directory with the given remotes.
131+ func newTestGitRepo (t * testing.T , remotes map [string ][]string ) * gitrepo.Repository {
132+ t .Helper ()
133+ dir := t .TempDir ()
134+
135+ r , err := git .PlainInit (dir , false )
136+ if err != nil {
137+ t .Fatalf ("git.PlainInit failed: %v" , err )
138+ }
139+
140+ for name , urls := range remotes {
141+ _ , err := r .CreateRemote (& gogitConfig.RemoteConfig {
142+ Name : name ,
143+ URLs : urls ,
144+ })
145+ if err != nil {
146+ t .Fatalf ("CreateRemote failed: %v" , err )
147+ }
148+ }
149+
150+ repo , err := gitrepo .NewRepository (& gitrepo.RepositoryOptions {Dir : dir })
151+ if err != nil {
152+ t .Fatalf ("gitrepo.NewRepository failed: %v" , err )
153+ }
154+ return repo
155+ }
156+
157+ func TestFetchGitHubRepoFromRemote (t * testing.T ) {
158+ t .Parallel ()
159+ for _ , test := range []struct {
160+ name string
161+ remotes map [string ][]string
162+ wantRepo * Repository
163+ wantErr bool
164+ wantErrSubstr string
165+ }{
166+ {
167+ name : "Single GitHub remote" ,
168+ remotes : map [string ][]string {
169+ "origin" : {"https://github.com/owner/repo.git" },
170+ },
171+ wantRepo : & Repository {Owner : "owner" , Name : "repo" },
172+ },
173+ {
174+ name : "No remotes" ,
175+ remotes : map [string ][]string {},
176+ wantErr : true ,
177+ wantErrSubstr : "no GitHub remotes found" ,
178+ },
179+ {
180+ name : "No GitHub remotes" ,
181+ remotes : map [string ][]string {
182+ "origin" : {"https://gitlab.com/owner/repo.git" },
183+ },
184+ wantErr : true ,
185+ wantErrSubstr : "no GitHub remotes found" ,
186+ },
187+ {
188+ name : "Multiple remotes, one is GitHub" ,
189+ remotes : map [string ][]string {
190+ "gitlab" : {"https://gitlab.com/owner/repo.git" },
191+ "upstream" : {"https://github.com/gh-owner/gh-repo.git" },
192+ },
193+ wantRepo : & Repository {Owner : "gh-owner" , Name : "gh-repo" },
194+ },
195+ {
196+ name : "Multiple GitHub remotes" ,
197+ remotes : map [string ][]string {
198+ "origin" : {"https://github.com/owner/repo.git" },
199+ "upstream" : {"https://github.com/owner2/repo2.git" },
200+ },
201+ wantErr : true ,
202+ wantErrSubstr : "can only determine the GitHub repo with a single matching remote" ,
203+ },
204+ {
205+ name : "Remote with multiple URLs, first is not GitHub" ,
206+ remotes : map [string ][]string {
207+ "origin" : {"https://gitlab.com/owner/repo.git" , "https://github.com/owner/repo.git" },
208+ },
209+ wantErr : true ,
210+ wantErrSubstr : "no GitHub remotes found" ,
211+ },
212+ {
213+ name : "Remote with multiple URLs, first is GitHub" ,
214+ remotes : map [string ][]string {
215+ "origin" : {"https://github.com/owner/repo.git" , "https://gitlab.com/owner/repo.git" },
216+ },
217+ wantRepo : & Repository {Owner : "owner" , Name : "repo" },
218+ },
219+ } {
220+ t .Run (test .name , func (t * testing.T ) {
221+ t .Parallel ()
222+ repo := newTestGitRepo (t , test .remotes )
223+
224+ got , err := FetchGitHubRepoFromRemote (repo )
225+
226+ if test .wantErr {
227+ if err == nil {
228+ t .Errorf ("FetchGitHubRepoFromRemote() err = nil, want error containing %q" , test .wantErrSubstr )
229+ } else if ! strings .Contains (err .Error (), test .wantErrSubstr ) {
230+ t .Errorf ("FetchGitHubRepoFromRemote() err = %v, want error containing %q" , err , test .wantErrSubstr )
231+ }
232+ } else {
233+ if err != nil {
234+ t .Errorf ("FetchGitHubRepoFromRemote() err = %v, want nil" , err )
235+ }
236+ if diff := cmp .Diff (test .wantRepo , got ); diff != "" {
237+ t .Errorf ("FetchGitHubRepoFromRemote() repo mismatch (-want +got): %s" , diff )
238+ }
239+ }
240+ })
241+ }
242+ }
243+
125244func TestParseUrl (t * testing.T ) {
126245 t .Parallel ()
127246 for _ , test := range []struct {
@@ -158,7 +277,7 @@ func TestParseUrl(t *testing.T) {
158277 } {
159278 t .Run (test .name , func (t * testing.T ) {
160279 t .Parallel ()
161- repo , err := ParseUrl (test .remoteUrl )
280+ repo , err := ParseURL (test .remoteUrl )
162281
163282 if test .wantErr {
164283 if err == nil {
@@ -177,3 +296,106 @@ func TestParseUrl(t *testing.T) {
177296 })
178297 }
179298}
299+
300+ func TestCreatePullRequest (t * testing.T ) {
301+ t .Parallel ()
302+ for _ , test := range []struct {
303+ name string
304+ remoteBranch string
305+ title string
306+ body string
307+ handler http.HandlerFunc
308+ wantMetadata * PullRequestMetadata
309+ wantErr bool
310+ wantErrSubstr string
311+ }{
312+ {
313+ name : "Success with provided body" ,
314+ remoteBranch : "feature-branch" ,
315+ title : "New Feature" ,
316+ body : "This is a new feature." ,
317+ handler : func (w http.ResponseWriter , r * http.Request ) {
318+ if r .Method != http .MethodPost {
319+ t .Errorf ("unexpected method: got %s, want %s" , r .Method , http .MethodPost )
320+ }
321+ if r .URL .Path != "/repos/owner/repo/pulls" {
322+ t .Errorf ("unexpected path: got %s, want %s" , r .URL .Path , "/repos/owner/repo/pulls" )
323+ }
324+ var newPR github.NewPullRequest
325+ if err := json .NewDecoder (r .Body ).Decode (& newPR ); err != nil {
326+ t .Fatalf ("failed to decode request body: %v" , err )
327+ }
328+ if * newPR .Title != "New Feature" {
329+ t .Errorf ("unexpected title: got %q, want %q" , * newPR .Title , "New Feature" )
330+ }
331+ if * newPR .Body != "This is a new feature." {
332+ t .Errorf ("unexpected body: got %q, want %q" , * newPR .Body , "This is a new feature." )
333+ }
334+ if * newPR .Head != "feature-branch" {
335+ t .Errorf ("unexpected head: got %q, want %q" , * newPR .Head , "feature-branch" )
336+ }
337+ if * newPR .Base != "main" {
338+ t .Errorf ("unexpected base: got %q, want %q" , * newPR .Base , "main" )
339+ }
340+ fmt .Fprint (w , `{"number": 1, "html_url": "https://github.com/owner/repo/pull/1"}` )
341+ },
342+ wantMetadata : & PullRequestMetadata {Repo : & Repository {Owner : "owner" , Name : "repo" }, Number : 1 },
343+ },
344+ {
345+ name : "Success with empty body" ,
346+ remoteBranch : "another-branch" ,
347+ title : "Another PR" ,
348+ body : "" ,
349+ handler : func (w http.ResponseWriter , r * http.Request ) {
350+ var newPR github.NewPullRequest
351+ if err := json .NewDecoder (r .Body ).Decode (& newPR ); err != nil {
352+ t .Fatalf ("failed to decode request body: %v" , err )
353+ }
354+ expectedBody := "Regenerated all changed APIs. See individual commits for details."
355+ if * newPR .Body != expectedBody {
356+ t .Errorf ("unexpected body: got %q, want %q" , * newPR .Body , expectedBody )
357+ }
358+ fmt .Fprint (w , `{"number": 1, "html_url": "https://github.com/owner/repo/pull/1"}` )
359+ },
360+ wantMetadata : & PullRequestMetadata {Repo : & Repository {Owner : "owner" , Name : "repo" }, Number : 1 },
361+ },
362+ {
363+ name : "GitHub API error" ,
364+ remoteBranch : "error-branch" ,
365+ title : "Error PR" ,
366+ body : "This will fail." ,
367+ handler : func (w http.ResponseWriter , r * http.Request ) { w .WriteHeader (http .StatusInternalServerError ) },
368+ wantErr : true ,
369+ wantErrSubstr : "500" ,
370+ },
371+ } {
372+ t .Run (test .name , func (t * testing.T ) {
373+ t .Parallel ()
374+ server := httptest .NewServer (test .handler )
375+ defer server .Close ()
376+
377+ repo := & Repository {Owner : "owner" , Name : "repo" }
378+ client , err := newClientWithHTTP ("fake-token" , repo , server .Client ())
379+ if err != nil {
380+ t .Fatalf ("newClientWithHTTP() error = %v" , err )
381+ }
382+ client .BaseURL , _ = url .Parse (server .URL + "/" )
383+
384+ metadata , err := client .CreatePullRequest (context .Background (), repo , test .remoteBranch , test .title , test .body )
385+
386+ if test .wantErr {
387+ if err == nil {
388+ t .Errorf ("CreatePullRequest() err = nil, want error containing %q" , test .wantErrSubstr )
389+ } else if ! strings .Contains (err .Error (), test .wantErrSubstr ) {
390+ t .Errorf ("CreatePullRequest() err = %v, want error containing %q" , err , test .wantErrSubstr )
391+ }
392+ } else if err != nil {
393+ t .Errorf ("CreatePullRequest() err = %v, want nil" , err )
394+ }
395+
396+ if diff := cmp .Diff (test .wantMetadata , metadata ); diff != "" {
397+ t .Errorf ("CreatePullRequest() metadata mismatch (-want +got):\n %s" , diff )
398+ }
399+ })
400+ }
401+ }
0 commit comments