github.com/goreleaser/goreleaser@v1.25.1/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/v61/github" 17 "github.com/goreleaser/goreleaser/internal/artifact" 18 "github.com/goreleaser/goreleaser/internal/testctx" 19 "github.com/goreleaser/goreleaser/internal/testlib" 20 "github.com/goreleaser/goreleaser/pkg/config" 21 "github.com/stretchr/testify/require" 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.True(t, 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 TestGitHubCreateFileFeatureBranchAlreadyExists(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.StatusUnprocessableEntity) 827 fmt.Fprintf(w, `{"message": "Reference already exists"}`) 828 return 829 } 830 831 if r.URL.Path == "/repos/someone/something" { 832 w.WriteHeader(http.StatusOK) 833 fmt.Fprint(w, `{"default_branch": "main"}`) 834 return 835 } 836 837 if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet { 838 w.WriteHeader(http.StatusNotFound) 839 return 840 } 841 842 if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut { 843 w.WriteHeader(http.StatusOK) 844 return 845 } 846 847 if r.URL.Path == "/rate_limit" { 848 w.WriteHeader(http.StatusOK) 849 fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`) 850 return 851 } 852 853 t.Error("unhandled request: " + r.Method + " " + r.URL.Path) 854 })) 855 defer srv.Close() 856 857 ctx := testctx.NewWithCfg(config.Project{ 858 GitHubURLs: config.GitHubURLs{ 859 API: srv.URL + "/", 860 }, 861 }) 862 client, err := newGitHub(ctx, "test-token") 863 require.NoError(t, err) 864 repo := Repo{ 865 Owner: "someone", 866 Name: "something", 867 Branch: "feature", 868 } 869 870 require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message")) 871 } 872 873 func TestGitHubCreateFileFeatureBranchDoesNotExist(t *testing.T) { 874 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 875 defer r.Body.Close() 876 877 if r.URL.Path == "/repos/someone/something/branches/feature" && r.Method == http.MethodGet { 878 w.WriteHeader(http.StatusNotFound) 879 return 880 } 881 882 if r.URL.Path == "/repos/someone/something/git/ref/heads/main" { 883 fmt.Fprint(w, `{"object": {"sha": "fake-sha"}}`) 884 return 885 } 886 887 if r.URL.Path == "/repos/someone/something/git/refs" && r.Method == http.MethodPost { 888 w.WriteHeader(http.StatusOK) 889 return 890 } 891 892 if r.URL.Path == "/repos/someone/something" { 893 w.WriteHeader(http.StatusOK) 894 fmt.Fprint(w, `{"default_branch": "main"}`) 895 return 896 } 897 898 if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet { 899 w.WriteHeader(http.StatusNotFound) 900 return 901 } 902 903 if r.URL.Path == "/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut { 904 w.WriteHeader(http.StatusOK) 905 return 906 } 907 908 if r.URL.Path == "/rate_limit" { 909 w.WriteHeader(http.StatusOK) 910 fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`) 911 return 912 } 913 914 t.Error("unhandled request: " + r.Method + " " + r.URL.Path) 915 })) 916 defer srv.Close() 917 918 ctx := testctx.NewWithCfg(config.Project{ 919 GitHubURLs: config.GitHubURLs{ 920 API: srv.URL + "/", 921 }, 922 }) 923 client, err := newGitHub(ctx, "test-token") 924 require.NoError(t, err) 925 repo := Repo{ 926 Owner: "someone", 927 Name: "something", 928 Branch: "feature", 929 } 930 931 require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message")) 932 } 933 934 func TestGitHubCheckRateLimit(t *testing.T) { 935 now := time.Now().UTC() 936 reset := now.Add(1392 * time.Millisecond) 937 var first atomic.Bool 938 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 939 defer r.Body.Close() 940 if r.URL.Path == "/rate_limit" { 941 w.WriteHeader(http.StatusOK) 942 resetstr, _ := github.Timestamp{Time: reset}.MarshalJSON() 943 if first.Load() { 944 // second time asking for the rate limit 945 fmt.Fprintf(w, `{"resources":{"core":{"remaining":138,"reset":%s}}}`, string(resetstr)) 946 return 947 } 948 949 // first time asking for the rate limit 950 fmt.Fprintf(w, `{"resources":{"core":{"remaining":98,"reset":%s}}}`, string(resetstr)) 951 first.Store(true) 952 return 953 } 954 t.Error("unhandled request: " + r.Method + " " + r.URL.Path) 955 })) 956 defer srv.Close() 957 958 ctx := testctx.NewWithCfg(config.Project{ 959 GitHubURLs: config.GitHubURLs{ 960 API: srv.URL + "/", 961 }, 962 }) 963 client, err := newGitHub(ctx, "test-token") 964 require.NoError(t, err) 965 client.checkRateLimit(ctx) 966 require.True(t, time.Now().UTC().After(reset)) 967 } 968 969 // TODO: test create release 970 // TODO: test create upload file to release 971 // TODO: test delete draft release 972 // TODO: test create PR