github.com/windmeup/goreleaser@v1.21.95/internal/client/github_test.go (about)

     1  package client
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"os"
    11  	"sync/atomic"
    12  	"testing"
    13  	"text/template"
    14  	"time"
    15  
    16  	"github.com/google/go-github/v56/github"
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/windmeup/goreleaser/internal/artifact"
    19  	"github.com/windmeup/goreleaser/internal/testctx"
    20  	"github.com/windmeup/goreleaser/internal/testlib"
    21  	"github.com/windmeup/goreleaser/pkg/config"
    22  )
    23  
    24  func TestNewGitHubClient(t *testing.T) {
    25  	t.Run("good urls", func(t *testing.T) {
    26  		githubURL := "https://github.mycompany.com"
    27  		ctx := testctx.NewWithCfg(config.Project{
    28  			GitHubURLs: config.GitHubURLs{
    29  				API:    githubURL + "/api",
    30  				Upload: githubURL + "/upload",
    31  			},
    32  		})
    33  
    34  		client, err := newGitHub(ctx, ctx.Token)
    35  		require.NoError(t, err)
    36  		require.Equal(t, githubURL+"/api", client.client.BaseURL.String())
    37  		require.Equal(t, githubURL+"/upload", client.client.UploadURL.String())
    38  	})
    39  
    40  	t.Run("bad api url", func(t *testing.T) {
    41  		ctx := testctx.NewWithCfg(config.Project{
    42  			GitHubURLs: config.GitHubURLs{
    43  				API:    "://github.mycompany.com/api",
    44  				Upload: "https://github.mycompany.com/upload",
    45  			},
    46  		})
    47  		_, err := newGitHub(ctx, ctx.Token)
    48  
    49  		require.EqualError(t, err, `parse "://github.mycompany.com/api": missing protocol scheme`)
    50  	})
    51  
    52  	t.Run("bad upload url", func(t *testing.T) {
    53  		ctx := testctx.NewWithCfg(config.Project{
    54  			GitHubURLs: config.GitHubURLs{
    55  				API:    "https://github.mycompany.com/api",
    56  				Upload: "not a url:4994",
    57  			},
    58  		})
    59  		_, err := newGitHub(ctx, ctx.Token)
    60  
    61  		require.EqualError(t, err, `parse "not a url:4994": first path segment in URL cannot contain colon`)
    62  	})
    63  
    64  	t.Run("template", func(t *testing.T) {
    65  		githubURL := "https://github.mycompany.com"
    66  		ctx := testctx.NewWithCfg(config.Project{
    67  			Env: []string{
    68  				fmt.Sprintf("GORELEASER_TEST_GITHUB_URLS_API=%s/api", githubURL),
    69  				fmt.Sprintf("GORELEASER_TEST_GITHUB_URLS_UPLOAD=%s/upload", githubURL),
    70  			},
    71  			GitHubURLs: config.GitHubURLs{
    72  				API:    "{{ .Env.GORELEASER_TEST_GITHUB_URLS_API }}",
    73  				Upload: "{{ .Env.GORELEASER_TEST_GITHUB_URLS_UPLOAD }}",
    74  			},
    75  		})
    76  
    77  		client, err := newGitHub(ctx, ctx.Token)
    78  		require.NoError(t, err)
    79  		require.Equal(t, githubURL+"/api", client.client.BaseURL.String())
    80  		require.Equal(t, githubURL+"/upload", client.client.UploadURL.String())
    81  	})
    82  
    83  	t.Run("template invalid api", func(t *testing.T) {
    84  		ctx := testctx.NewWithCfg(config.Project{
    85  			GitHubURLs: config.GitHubURLs{
    86  				API: "{{ .Env.GORELEASER_NOT_EXISTS }}",
    87  			},
    88  		})
    89  
    90  		_, err := newGitHub(ctx, ctx.Token)
    91  		require.ErrorAs(t, err, &template.ExecError{})
    92  	})
    93  
    94  	t.Run("template invalid upload", func(t *testing.T) {
    95  		ctx := testctx.NewWithCfg(config.Project{
    96  			GitHubURLs: config.GitHubURLs{
    97  				API:    "https://github.mycompany.com/api",
    98  				Upload: "{{ .Env.GORELEASER_NOT_EXISTS }}",
    99  			},
   100  		})
   101  
   102  		_, err := newGitHub(ctx, ctx.Token)
   103  		require.ErrorAs(t, err, &template.ExecError{})
   104  	})
   105  
   106  	t.Run("template invalid", func(t *testing.T) {
   107  		ctx := testctx.NewWithCfg(config.Project{
   108  			GitHubURLs: config.GitHubURLs{
   109  				API: "{{.dddddddddd",
   110  			},
   111  		})
   112  
   113  		_, err := newGitHub(ctx, ctx.Token)
   114  		require.Error(t, err)
   115  	})
   116  }
   117  
   118  func TestGitHubUploadReleaseIDNotInt(t *testing.T) {
   119  	ctx := testctx.New()
   120  	client, err := newGitHub(ctx, ctx.Token)
   121  	require.NoError(t, err)
   122  
   123  	require.EqualError(
   124  		t,
   125  		client.Upload(ctx, "blah", &artifact.Artifact{}, nil),
   126  		`strconv.ParseInt: parsing "blah": invalid syntax`,
   127  	)
   128  }
   129  
   130  func TestGitHubReleaseURLTemplate(t *testing.T) {
   131  	tests := []struct {
   132  		name            string
   133  		downloadURL     string
   134  		wantDownloadURL string
   135  		wantErr         bool
   136  	}{
   137  		{
   138  			name:            "default_download_url",
   139  			downloadURL:     DefaultGitHubDownloadURL,
   140  			wantDownloadURL: "https://github.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
   141  		},
   142  		{
   143  			name:            "download_url_template",
   144  			downloadURL:     "{{ .Env.GORELEASER_TEST_GITHUB_URLS_DOWNLOAD }}",
   145  			wantDownloadURL: "https://github.mycompany.com/owner/name/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
   146  		},
   147  		{
   148  			name:        "download_url_template_invalid_value",
   149  			downloadURL: "{{ .Env.GORELEASER_NOT_EXISTS }}",
   150  			wantErr:     true,
   151  		},
   152  		{
   153  			name:        "download_url_template_invalid",
   154  			downloadURL: "{{.dddddddddd",
   155  			wantErr:     true,
   156  		},
   157  	}
   158  
   159  	for _, tt := range tests {
   160  		t.Run(tt.name, func(t *testing.T) {
   161  			ctx := testctx.NewWithCfg(config.Project{
   162  				Env: []string{
   163  					"GORELEASER_TEST_GITHUB_URLS_DOWNLOAD=https://github.mycompany.com",
   164  				},
   165  				GitHubURLs: config.GitHubURLs{
   166  					Download: tt.downloadURL,
   167  				},
   168  				Release: config.Release{
   169  					GitHub: config.Repo{
   170  						Owner: "owner",
   171  						Name:  "name",
   172  					},
   173  				},
   174  			})
   175  			client, err := newGitHub(ctx, ctx.Token)
   176  			require.NoError(t, err)
   177  
   178  			urlTpl, err := client.ReleaseURLTemplate(ctx)
   179  			if tt.wantErr {
   180  				require.Error(t, err)
   181  				return
   182  			}
   183  
   184  			require.NoError(t, err)
   185  			require.Equal(t, tt.wantDownloadURL, urlTpl)
   186  		})
   187  	}
   188  }
   189  
   190  func TestGitHubCreateReleaseWrongNameTemplate(t *testing.T) {
   191  	ctx := testctx.NewWithCfg(config.Project{
   192  		Release: config.Release{
   193  			NameTemplate: "{{.dddddddddd",
   194  		},
   195  	})
   196  	client, err := newGitHub(ctx, ctx.Token)
   197  	require.NoError(t, err)
   198  
   199  	str, err := client.CreateRelease(ctx, "")
   200  	require.Empty(t, str)
   201  	testlib.RequireTemplateError(t, err)
   202  }
   203  
   204  func TestGitHubGetDefaultBranch(t *testing.T) {
   205  	totalRequests := 0
   206  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   207  		totalRequests++
   208  		defer r.Body.Close()
   209  
   210  		if r.URL.Path == "/rate_limit" {
   211  			w.WriteHeader(http.StatusOK)
   212  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   213  			return
   214  		}
   215  
   216  		// Assume the request to create a branch was good
   217  		w.WriteHeader(http.StatusOK)
   218  		fmt.Fprint(w, `{"default_branch": "main"}`)
   219  	}))
   220  	defer srv.Close()
   221  
   222  	ctx := testctx.NewWithCfg(config.Project{
   223  		GitHubURLs: config.GitHubURLs{
   224  			API: srv.URL + "/",
   225  		},
   226  	})
   227  
   228  	client, err := newGitHub(ctx, "test-token")
   229  	require.NoError(t, err)
   230  	repo := Repo{
   231  		Owner:  "someone",
   232  		Name:   "something",
   233  		Branch: "somebranch",
   234  	}
   235  
   236  	b, err := client.getDefaultBranch(ctx, repo)
   237  	require.NoError(t, err)
   238  	require.Equal(t, "main", b)
   239  	require.Equal(t, 2, totalRequests)
   240  }
   241  
   242  func TestGitHubGetDefaultBranchErr(t *testing.T) {
   243  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   244  		defer r.Body.Close()
   245  
   246  		// Assume the request to create a branch was good
   247  		w.WriteHeader(http.StatusNotImplemented)
   248  		fmt.Fprint(w, "{}")
   249  	}))
   250  	defer srv.Close()
   251  
   252  	ctx := testctx.NewWithCfg(config.Project{
   253  		GitHubURLs: config.GitHubURLs{
   254  			API: srv.URL + "/",
   255  		},
   256  	})
   257  	client, err := newGitHub(ctx, "test-token")
   258  	require.NoError(t, err)
   259  	repo := Repo{
   260  		Owner:  "someone",
   261  		Name:   "something",
   262  		Branch: "somebranch",
   263  	}
   264  
   265  	_, err = client.getDefaultBranch(ctx, repo)
   266  	require.Error(t, err)
   267  }
   268  
   269  func TestGitHubChangelog(t *testing.T) {
   270  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   271  		defer r.Body.Close()
   272  
   273  		if r.URL.Path == "/repos/someone/something/compare/v1.0.0...v1.1.0" {
   274  			r, err := os.Open("testdata/github/compare.json")
   275  			require.NoError(t, err)
   276  			_, err = io.Copy(w, r)
   277  			require.NoError(t, err)
   278  			return
   279  		}
   280  		if r.URL.Path == "/rate_limit" {
   281  			w.WriteHeader(http.StatusOK)
   282  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   283  			return
   284  		}
   285  	}))
   286  	defer srv.Close()
   287  
   288  	ctx := testctx.NewWithCfg(config.Project{
   289  		GitHubURLs: config.GitHubURLs{
   290  			API: srv.URL + "/",
   291  		},
   292  	})
   293  	client, err := newGitHub(ctx, "test-token")
   294  	require.NoError(t, err)
   295  	repo := Repo{
   296  		Owner:  "someone",
   297  		Name:   "something",
   298  		Branch: "somebranch",
   299  	}
   300  
   301  	log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
   302  	require.NoError(t, err)
   303  	require.Equal(t, "6dcb09b5b57875f334f61aebed695e2e4193db5e: Fix all the bugs (@octocat)", log)
   304  }
   305  
   306  func TestGitHubReleaseNotes(t *testing.T) {
   307  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   308  		defer r.Body.Close()
   309  
   310  		if r.URL.Path == "/repos/someone/something/releases/generate-notes" {
   311  			r, err := os.Open("testdata/github/releasenotes.json")
   312  			require.NoError(t, err)
   313  			_, err = io.Copy(w, r)
   314  			require.NoError(t, err)
   315  			return
   316  		}
   317  		if r.URL.Path == "/rate_limit" {
   318  			w.WriteHeader(http.StatusOK)
   319  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   320  			return
   321  		}
   322  	}))
   323  	defer srv.Close()
   324  
   325  	ctx := testctx.NewWithCfg(config.Project{
   326  		GitHubURLs: config.GitHubURLs{
   327  			API: srv.URL + "/",
   328  		},
   329  	})
   330  	client, err := newGitHub(ctx, "test-token")
   331  	require.NoError(t, err)
   332  	repo := Repo{
   333  		Owner:  "someone",
   334  		Name:   "something",
   335  		Branch: "somebranch",
   336  	}
   337  
   338  	log, err := client.GenerateReleaseNotes(ctx, repo, "v1.0.0", "v1.1.0")
   339  	require.NoError(t, err)
   340  	require.Equal(t, "**Full Changelog**: https://github.com/someone/something/compare/v1.0.0...v1.1.0", log)
   341  }
   342  
   343  func TestGitHubReleaseNotesError(t *testing.T) {
   344  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   345  		defer r.Body.Close()
   346  
   347  		if r.URL.Path == "/repos/someone/something/releases/generate-notes" {
   348  			w.WriteHeader(http.StatusBadRequest)
   349  		}
   350  		if r.URL.Path == "/rate_limit" {
   351  			w.WriteHeader(http.StatusOK)
   352  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   353  			return
   354  		}
   355  	}))
   356  	defer srv.Close()
   357  
   358  	ctx := testctx.NewWithCfg(config.Project{
   359  		GitHubURLs: config.GitHubURLs{
   360  			API: srv.URL + "/",
   361  		},
   362  	})
   363  	client, err := newGitHub(ctx, "test-token")
   364  	require.NoError(t, err)
   365  	repo := Repo{
   366  		Owner:  "someone",
   367  		Name:   "something",
   368  		Branch: "somebranch",
   369  	}
   370  
   371  	_, err = client.GenerateReleaseNotes(ctx, repo, "v1.0.0", "v1.1.0")
   372  	require.Error(t, err)
   373  }
   374  
   375  func TestGitHubCloseMilestone(t *testing.T) {
   376  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   377  		defer r.Body.Close()
   378  		t.Log(r.URL.Path)
   379  
   380  		if r.URL.Path == "/repos/someone/something/milestones" {
   381  			r, err := os.Open("testdata/github/milestones.json")
   382  			require.NoError(t, err)
   383  			_, err = io.Copy(w, r)
   384  			require.NoError(t, err)
   385  			return
   386  		}
   387  
   388  		if r.URL.Path == "/rate_limit" {
   389  			w.WriteHeader(http.StatusOK)
   390  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   391  			return
   392  		}
   393  	}))
   394  	defer srv.Close()
   395  
   396  	ctx := testctx.NewWithCfg(config.Project{
   397  		GitHubURLs: config.GitHubURLs{
   398  			API: srv.URL + "/",
   399  		},
   400  	})
   401  	client, err := newGitHub(ctx, "test-token")
   402  	require.NoError(t, err)
   403  	repo := Repo{
   404  		Owner: "someone",
   405  		Name:  "something",
   406  	}
   407  
   408  	require.NoError(t, client.CloseMilestone(ctx, repo, "v1.13.0"))
   409  }
   410  
   411  const testPRTemplate = "fake template\n- [ ] mark this\n---"
   412  
   413  func TestGitHubOpenPullRequestCrossRepo(t *testing.T) {
   414  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   415  		defer r.Body.Close()
   416  
   417  		if r.URL.Path == "/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
   418  			content := github.RepositoryContent{
   419  				Encoding: github.String("base64"),
   420  				Content:  github.String(base64.StdEncoding.EncodeToString([]byte(testPRTemplate))),
   421  			}
   422  			bts, _ := json.Marshal(content)
   423  			_, _ = w.Write(bts)
   424  			return
   425  		}
   426  
   427  		if r.URL.Path == "/repos/someone/something/pulls" {
   428  			got, err := io.ReadAll(r.Body)
   429  			require.NoError(t, err)
   430  			var pr github.NewPullRequest
   431  			require.NoError(t, json.Unmarshal(got, &pr))
   432  			require.Equal(t, "main", pr.GetBase())
   433  			require.Equal(t, "someoneelse:something:foo", pr.GetHead())
   434  			require.Equal(t, testPRTemplate+"\n"+prFooter, pr.GetBody())
   435  			r, err := os.Open("testdata/github/pull.json")
   436  			require.NoError(t, err)
   437  			_, err = io.Copy(w, r)
   438  			require.NoError(t, err)
   439  			return
   440  		}
   441  
   442  		if r.URL.Path == "/rate_limit" {
   443  			w.WriteHeader(http.StatusOK)
   444  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   445  			return
   446  		}
   447  
   448  		t.Error("unhandled request: " + r.URL.Path)
   449  	}))
   450  	defer srv.Close()
   451  
   452  	ctx := testctx.NewWithCfg(config.Project{
   453  		GitHubURLs: config.GitHubURLs{
   454  			API: srv.URL + "/",
   455  		},
   456  	})
   457  	client, err := newGitHub(ctx, "test-token")
   458  	require.NoError(t, err)
   459  	base := Repo{
   460  		Owner:  "someone",
   461  		Name:   "something",
   462  		Branch: "main",
   463  	}
   464  	head := Repo{
   465  		Owner:  "someoneelse",
   466  		Name:   "something",
   467  		Branch: "foo",
   468  	}
   469  	require.NoError(t, client.OpenPullRequest(ctx, base, head, "some title", false))
   470  }
   471  
   472  func TestGitHubOpenPullRequestHappyPath(t *testing.T) {
   473  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   474  		defer r.Body.Close()
   475  
   476  		if r.URL.Path == "/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
   477  			content := github.RepositoryContent{
   478  				Encoding: github.String("base64"),
   479  				Content:  github.String(base64.StdEncoding.EncodeToString([]byte(testPRTemplate))),
   480  			}
   481  			bts, _ := json.Marshal(content)
   482  			_, _ = w.Write(bts)
   483  			return
   484  		}
   485  
   486  		if r.URL.Path == "/repos/someone/something/pulls" {
   487  			r, err := os.Open("testdata/github/pull.json")
   488  			require.NoError(t, err)
   489  			_, err = io.Copy(w, r)
   490  			require.NoError(t, err)
   491  			return
   492  		}
   493  
   494  		if r.URL.Path == "/rate_limit" {
   495  			w.WriteHeader(http.StatusOK)
   496  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   497  			return
   498  		}
   499  
   500  		t.Error("unhandled request: " + r.URL.Path)
   501  	}))
   502  	defer srv.Close()
   503  
   504  	ctx := testctx.NewWithCfg(config.Project{
   505  		GitHubURLs: config.GitHubURLs{
   506  			API: srv.URL + "/",
   507  		},
   508  	})
   509  	client, err := newGitHub(ctx, "test-token")
   510  	require.NoError(t, err)
   511  	repo := Repo{
   512  		Owner:  "someone",
   513  		Name:   "something",
   514  		Branch: "main",
   515  	}
   516  
   517  	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title", false))
   518  }
   519  
   520  func TestGitHubOpenPullRequestNoBaseBranchDraft(t *testing.T) {
   521  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   522  		defer r.Body.Close()
   523  
   524  		if r.URL.Path == "/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
   525  			w.WriteHeader(http.StatusNotFound)
   526  			return
   527  		}
   528  
   529  		if r.URL.Path == "/repos/someone/something/pulls" {
   530  			got, err := io.ReadAll(r.Body)
   531  			require.NoError(t, err)
   532  			var pr github.NewPullRequest
   533  			require.NoError(t, json.Unmarshal(got, &pr))
   534  			require.Equal(t, "main", pr.GetBase())
   535  			require.Equal(t, "someone:something:foo", pr.GetHead())
   536  			require.Equal(t, true, pr.GetDraft())
   537  
   538  			r, err := os.Open("testdata/github/pull.json")
   539  			require.NoError(t, err)
   540  			_, err = io.Copy(w, r)
   541  			require.NoError(t, err)
   542  			return
   543  		}
   544  
   545  		if r.URL.Path == "/repos/someone/something" {
   546  			w.WriteHeader(http.StatusOK)
   547  			fmt.Fprint(w, `{"default_branch": "main"}`)
   548  			return
   549  		}
   550  
   551  		if r.URL.Path == "/rate_limit" {
   552  			w.WriteHeader(http.StatusOK)
   553  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   554  			return
   555  		}
   556  
   557  		t.Error("unhandled request: " + r.URL.Path)
   558  	}))
   559  	defer srv.Close()
   560  
   561  	ctx := testctx.NewWithCfg(config.Project{
   562  		GitHubURLs: config.GitHubURLs{
   563  			API: srv.URL + "/",
   564  		},
   565  	})
   566  	client, err := newGitHub(ctx, "test-token")
   567  	require.NoError(t, err)
   568  	repo := Repo{
   569  		Owner: "someone",
   570  		Name:  "something",
   571  	}
   572  
   573  	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{
   574  		Branch: "foo",
   575  	}, "some title", true))
   576  }
   577  
   578  func TestGitHubOpenPullRequestPRExists(t *testing.T) {
   579  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   580  		defer r.Body.Close()
   581  
   582  		if r.URL.Path == "/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
   583  			w.WriteHeader(http.StatusNotFound)
   584  			return
   585  		}
   586  
   587  		if r.URL.Path == "/repos/someone/something/pulls" {
   588  			w.WriteHeader(http.StatusUnprocessableEntity)
   589  			r, err := os.Open("testdata/github/pull.json")
   590  			require.NoError(t, err)
   591  			_, err = io.Copy(w, r)
   592  			require.NoError(t, err)
   593  			return
   594  		}
   595  
   596  		if r.URL.Path == "/rate_limit" {
   597  			w.WriteHeader(http.StatusOK)
   598  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   599  			return
   600  		}
   601  
   602  		t.Error("unhandled request: " + r.URL.Path)
   603  	}))
   604  	defer srv.Close()
   605  
   606  	ctx := testctx.NewWithCfg(config.Project{
   607  		GitHubURLs: config.GitHubURLs{
   608  			API: srv.URL + "/",
   609  		},
   610  	})
   611  	client, err := newGitHub(ctx, "test-token")
   612  	require.NoError(t, err)
   613  	repo := Repo{
   614  		Owner:  "someone",
   615  		Name:   "something",
   616  		Branch: "main",
   617  	}
   618  
   619  	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title", false))
   620  }
   621  
   622  func TestGitHubOpenPullRequestBaseEmpty(t *testing.T) {
   623  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   624  		defer r.Body.Close()
   625  
   626  		if r.URL.Path == "/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
   627  			w.WriteHeader(http.StatusNotFound)
   628  			return
   629  		}
   630  
   631  		if r.URL.Path == "/repos/someone/something/pulls" {
   632  			r, err := os.Open("testdata/github/pull.json")
   633  			require.NoError(t, err)
   634  			_, err = io.Copy(w, r)
   635  			require.NoError(t, err)
   636  			return
   637  		}
   638  
   639  		if r.URL.Path == "/repos/someone/something" {
   640  			w.WriteHeader(http.StatusOK)
   641  			fmt.Fprint(w, `{"default_branch": "main"}`)
   642  			return
   643  		}
   644  
   645  		if r.URL.Path == "/rate_limit" {
   646  			w.WriteHeader(http.StatusOK)
   647  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   648  			return
   649  		}
   650  
   651  		t.Error("unhandled request: " + r.URL.Path)
   652  	}))
   653  	defer srv.Close()
   654  
   655  	ctx := testctx.NewWithCfg(config.Project{
   656  		GitHubURLs: config.GitHubURLs{
   657  			API: srv.URL + "/",
   658  		},
   659  	})
   660  	client, err := newGitHub(ctx, "test-token")
   661  	require.NoError(t, err)
   662  	repo := Repo{
   663  		Owner:  "someone",
   664  		Name:   "something",
   665  		Branch: "foo",
   666  	}
   667  
   668  	require.NoError(t, client.OpenPullRequest(ctx, Repo{}, repo, "some title", false))
   669  }
   670  
   671  func TestGitHubOpenPullRequestHeadEmpty(t *testing.T) {
   672  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   673  		defer r.Body.Close()
   674  
   675  		if r.URL.Path == "/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
   676  			w.WriteHeader(http.StatusNotFound)
   677  			return
   678  		}
   679  
   680  		if r.URL.Path == "/repos/someone/something/pulls" {
   681  			r, err := os.Open("testdata/github/pull.json")
   682  			require.NoError(t, err)
   683  			_, err = io.Copy(w, r)
   684  			require.NoError(t, err)
   685  			return
   686  		}
   687  
   688  		if r.URL.Path == "/repos/someone/something" {
   689  			w.WriteHeader(http.StatusOK)
   690  			fmt.Fprint(w, `{"default_branch": "main"}`)
   691  			return
   692  		}
   693  
   694  		if r.URL.Path == "/rate_limit" {
   695  			w.WriteHeader(http.StatusOK)
   696  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   697  			return
   698  		}
   699  
   700  		t.Error("unhandled request: " + r.URL.Path)
   701  	}))
   702  	defer srv.Close()
   703  
   704  	ctx := testctx.NewWithCfg(config.Project{
   705  		GitHubURLs: config.GitHubURLs{
   706  			API: srv.URL + "/",
   707  		},
   708  	})
   709  	client, err := newGitHub(ctx, "test-token")
   710  	require.NoError(t, err)
   711  	repo := Repo{
   712  		Owner:  "someone",
   713  		Name:   "something",
   714  		Branch: "main",
   715  	}
   716  
   717  	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title", false))
   718  }
   719  
   720  func TestGitHubCreateFileHappyPathCreate(t *testing.T) {
   721  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   722  		defer r.Body.Close()
   723  
   724  		if r.URL.Path == "/repos/someone/something" {
   725  			w.WriteHeader(http.StatusOK)
   726  			fmt.Fprint(w, `{"default_branch": "main"}`)
   727  			return
   728  		}
   729  
   730  		if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
   731  			w.WriteHeader(http.StatusNotFound)
   732  			return
   733  		}
   734  
   735  		if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
   736  			w.WriteHeader(http.StatusOK)
   737  			return
   738  		}
   739  
   740  		if r.URL.Path == "/rate_limit" {
   741  			w.WriteHeader(http.StatusOK)
   742  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   743  			return
   744  		}
   745  
   746  		t.Error("unhandled request: " + r.URL.Path)
   747  	}))
   748  	defer srv.Close()
   749  
   750  	ctx := testctx.NewWithCfg(config.Project{
   751  		GitHubURLs: config.GitHubURLs{
   752  			API: srv.URL + "/",
   753  		},
   754  	})
   755  	client, err := newGitHub(ctx, "test-token")
   756  	require.NoError(t, err)
   757  	repo := Repo{
   758  		Owner: "someone",
   759  		Name:  "something",
   760  	}
   761  
   762  	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
   763  }
   764  
   765  func TestGitHubCreateFileHappyPathUpdate(t *testing.T) {
   766  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   767  		defer r.Body.Close()
   768  
   769  		if r.URL.Path == "/repos/someone/something" {
   770  			w.WriteHeader(http.StatusOK)
   771  			fmt.Fprint(w, `{"default_branch": "main"}`)
   772  			return
   773  		}
   774  
   775  		if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
   776  			w.WriteHeader(http.StatusOK)
   777  			fmt.Fprint(w, `{"sha": "fake"}`)
   778  			return
   779  		}
   780  
   781  		if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
   782  			w.WriteHeader(http.StatusOK)
   783  			return
   784  		}
   785  
   786  		if r.URL.Path == "/rate_limit" {
   787  			w.WriteHeader(http.StatusOK)
   788  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   789  			return
   790  		}
   791  
   792  		t.Error("unhandled request: " + r.URL.Path)
   793  	}))
   794  	defer srv.Close()
   795  
   796  	ctx := testctx.NewWithCfg(config.Project{
   797  		GitHubURLs: config.GitHubURLs{
   798  			API: srv.URL + "/",
   799  		},
   800  	})
   801  	client, err := newGitHub(ctx, "test-token")
   802  	require.NoError(t, err)
   803  	repo := Repo{
   804  		Owner: "someone",
   805  		Name:  "something",
   806  	}
   807  
   808  	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
   809  }
   810  
   811  func TestGitHubCreateFileFeatureBranchDoesNotExist(t *testing.T) {
   812  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   813  		defer r.Body.Close()
   814  
   815  		if r.URL.Path == "/repos/someone/something/branches/feature" && r.Method == http.MethodGet {
   816  			w.WriteHeader(http.StatusNotFound)
   817  			return
   818  		}
   819  
   820  		if r.URL.Path == "/repos/someone/something/git/ref/heads/main" {
   821  			fmt.Fprint(w, `{"object": {"sha": "fake-sha"}}`)
   822  			return
   823  		}
   824  
   825  		if r.URL.Path == "/repos/someone/something/git/refs" && r.Method == http.MethodPost {
   826  			w.WriteHeader(http.StatusOK)
   827  			return
   828  		}
   829  
   830  		if r.URL.Path == "/repos/someone/something" {
   831  			w.WriteHeader(http.StatusOK)
   832  			fmt.Fprint(w, `{"default_branch": "main"}`)
   833  			return
   834  		}
   835  
   836  		if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
   837  			w.WriteHeader(http.StatusNotFound)
   838  			return
   839  		}
   840  
   841  		if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
   842  			w.WriteHeader(http.StatusOK)
   843  			return
   844  		}
   845  
   846  		if r.URL.Path == "/rate_limit" {
   847  			w.WriteHeader(http.StatusOK)
   848  			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
   849  			return
   850  		}
   851  
   852  		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
   853  	}))
   854  	defer srv.Close()
   855  
   856  	ctx := testctx.NewWithCfg(config.Project{
   857  		GitHubURLs: config.GitHubURLs{
   858  			API: srv.URL + "/",
   859  		},
   860  	})
   861  	client, err := newGitHub(ctx, "test-token")
   862  	require.NoError(t, err)
   863  	repo := Repo{
   864  		Owner:  "someone",
   865  		Name:   "something",
   866  		Branch: "feature",
   867  	}
   868  
   869  	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
   870  }
   871  
   872  func TestGitHubCheckRateLimit(t *testing.T) {
   873  	now := time.Now().UTC()
   874  	reset := now.Add(1392 * time.Millisecond)
   875  	var first atomic.Bool
   876  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   877  		defer r.Body.Close()
   878  		if r.URL.Path == "/rate_limit" {
   879  			w.WriteHeader(http.StatusOK)
   880  			resetstr, _ := github.Timestamp{Time: reset}.MarshalJSON()
   881  			if first.Load() {
   882  				// second time asking for the rate limit
   883  				fmt.Fprintf(w, `{"resources":{"core":{"remaining":138,"reset":%s}}}`, string(resetstr))
   884  				return
   885  			}
   886  
   887  			// first time asking for the rate limit
   888  			fmt.Fprintf(w, `{"resources":{"core":{"remaining":98,"reset":%s}}}`, string(resetstr))
   889  			first.Store(true)
   890  			return
   891  		}
   892  		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
   893  	}))
   894  	defer srv.Close()
   895  
   896  	ctx := testctx.NewWithCfg(config.Project{
   897  		GitHubURLs: config.GitHubURLs{
   898  			API: srv.URL + "/",
   899  		},
   900  	})
   901  	client, err := newGitHub(ctx, "test-token")
   902  	require.NoError(t, err)
   903  	client.checkRateLimit(ctx)
   904  	require.True(t, time.Now().UTC().After(reset))
   905  }
   906  
   907  // TODO: test create release
   908  // TODO: test create upload file to release
   909  // TODO: test delete draft release
   910  // TODO: test create PR