package main import ( "context" "errors" "io" "net/http" "os" "path/filepath" "testing" "github.com/google/go-github/github" "github.com/grafana/grafana/pkg/build/cmd/util" "github.com/stretchr/testify/assert" "github.com/urfave/cli/v2" ) type githubPublishTestCases struct { name string args []string token string expectedError error errorContains string expectedOutput string mockedService *mockGithubRepositoryServiceImpl } var mockGithubRepositoryService = &mockGithubRepositoryServiceImpl{} func mockGithubRepositoryClient(context.Context, string) githubRepositoryService { return mockGithubRepositoryService } func TestPublishGithub(t *testing.T) { t.Setenv("DRONE_BUILD_EVENT", "promote") t.Setenv("DRONE_TAG", "v1.0.0") t.Setenv("DRONE_COMMIT", "abcdefgh") testApp, testPath := setupPublishGithubTests(t) mockErrUnauthorized := errors.New("401") testCases := []githubPublishTestCases{ { name: "try to publish without required flags", errorContains: `Required flags "path, repo" not set`, }, { name: "try to publish without token", args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"}, expectedError: errTokenIsEmpty, }, { name: "try to publish with invalid token", token: "invalid", args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"}, mockedService: &mockGithubRepositoryServiceImpl{tagErr: mockErrUnauthorized}, expectedError: mockErrUnauthorized, }, { name: "try to publish with valid token and nonexisting tag with create disabled", token: "valid", args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"}, mockedService: &mockGithubRepositoryServiceImpl{tagErr: errReleaseNotFound}, expectedError: errReleaseNotFound, }, { name: "try to publish with valid token and nonexisting tag with create enabled", token: "valid", args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0", "--create"}, mockedService: &mockGithubRepositoryServiceImpl{tagErr: errReleaseNotFound}, }, { name: "try to publish with valid token and existing tag", token: "valid", args: []string{"--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"}, }, { name: "dry run with invalid token", token: "invalid", args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"}, mockedService: &mockGithubRepositoryServiceImpl{tagErr: mockErrUnauthorized}, expectedOutput: "Github communication error", }, { name: "dry run with valid token and nonexisting tag with create disabled", token: "valid", args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"}, mockedService: &mockGithubRepositoryServiceImpl{tagErr: errReleaseNotFound}, expectedOutput: "Release doesn't exist", }, { name: "dry run with valid token and nonexisting tag with create enabled", token: "valid", args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0", "--create"}, mockedService: &mockGithubRepositoryServiceImpl{tagErr: errReleaseNotFound}, expectedOutput: "Would upload asset", }, { name: "dry run with valid token and existing tag", token: "valid", args: []string{"--dry-run", "--path", testPath, "--repo", "test/test", "--tag", "v1.0.0"}, expectedOutput: "Would upload asset", }, } if os.Getenv("DRONE_COMMIT") == "" { // this test only works locally due to Drone environment testCases = append(testCases, githubPublishTestCases{ name: "try to publish without tag", args: []string{"--path", testPath, "--repo", "test/test"}, expectedError: errTagIsEmpty, }, ) } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { if test.token != "" { t.Setenv("GH_TOKEN", test.token) } if test.mockedService != nil { mockGithubRepositoryService = test.mockedService } else { mockGithubRepositoryService = &mockGithubRepositoryServiceImpl{} } args := []string{"run"} args = append(args, test.args...) out, err := captureStdout(t, func() error { return testApp.Run(args) }) if test.expectedOutput != "" { assert.Contains(t, out, test.expectedOutput) } if test.expectedError != nil || test.errorContains != "" { assert.Error(t, err) if test.expectedError != nil { assert.ErrorIs(t, err, test.expectedError) } if test.errorContains != "" { assert.ErrorContains(t, err, test.errorContains) } } else { assert.NoError(t, err) } }) } } func setupPublishGithubTests(t *testing.T) (*cli.App, string) { t.Helper() ex, err := os.Executable() if err != nil { panic(err) } testPath := filepath.Dir(ex) newGithubClient = mockGithubRepositoryClient testApp := cli.NewApp() testApp.Action = PublishGithub testApp.Flags = []cli.Flag{ &util.DryRunFlag, &cli.StringFlag{ Name: "path", Required: true, Usage: "Path to the asset to be published", }, &cli.StringFlag{ Name: "repo", Required: true, Usage: "Github repository", }, &cli.StringFlag{ Name: "tag", Usage: "Release tag (default from metadata)ß", }, &cli.BoolFlag{ Name: "create", Usage: "Create release if it doesn't exist", }, } return testApp, testPath } func captureStdout(t *testing.T, fn func() error) (string, error) { t.Helper() rescueStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w err := fn() werr := w.Close() if werr != nil { return "", err } out, _ := io.ReadAll(r) os.Stdout = rescueStdout return string(out), err } type mockGithubRepositoryServiceImpl struct { tagErr error createErr error uploadErr error } func (m *mockGithubRepositoryServiceImpl) GetReleaseByTag(ctx context.Context, owner string, repo string, tag string) (*github.RepositoryRelease, *github.Response, error) { var release *github.RepositoryRelease res := &github.Response{Response: &http.Response{}} if m.tagErr == nil { releaseID := int64(1) release = &github.RepositoryRelease{ID: &releaseID} } else if errors.Is(m.tagErr, errReleaseNotFound) { res.StatusCode = 404 } return release, res, m.tagErr } func (m *mockGithubRepositoryServiceImpl) CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error) { releaseID := int64(1) return &github.RepositoryRelease{ID: &releaseID}, &github.Response{}, m.createErr } func (m *mockGithubRepositoryServiceImpl) UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error) { assetName := "test" assetUrl := "testurl.com.br" return &github.ReleaseAsset{Name: &assetName, BrowserDownloadURL: &assetUrl}, &github.Response{}, m.uploadErr } func (m *mockGithubRepositoryServiceImpl) DeleteReleaseAsset(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) { return nil, nil } func (m *mockGithubRepositoryServiceImpl) ListReleaseAssets(ctx context.Context, owner string, repo string, id int64, opts *github.ListOptions) ([]*github.ReleaseAsset, *github.Response, error) { resp := github.Response{} resp.LastPage = 1 resp.FirstPage = 1 resp.NextPage = 0 return []*github.ReleaseAsset{}, &resp, nil }