github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/release/create/create_test.go (about)

     1  package create
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"testing"
    13  
    14  	"github.com/cli/cli/internal/ghrepo"
    15  	"github.com/cli/cli/pkg/cmd/release/shared"
    16  	"github.com/cli/cli/pkg/cmdutil"
    17  	"github.com/cli/cli/pkg/httpmock"
    18  	"github.com/cli/cli/pkg/iostreams"
    19  	"github.com/google/shlex"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func Test_NewCmdCreate(t *testing.T) {
    25  	tempDir := t.TempDir()
    26  	tf, err := ioutil.TempFile(tempDir, "release-create")
    27  	require.NoError(t, err)
    28  	fmt.Fprint(tf, "MY NOTES")
    29  	tf.Close()
    30  	af1, err := os.Create(filepath.Join(tempDir, "windows.zip"))
    31  	require.NoError(t, err)
    32  	af1.Close()
    33  	af2, err := os.Create(filepath.Join(tempDir, "linux.tgz"))
    34  	require.NoError(t, err)
    35  	af2.Close()
    36  
    37  	tests := []struct {
    38  		name    string
    39  		args    string
    40  		isTTY   bool
    41  		stdin   string
    42  		want    CreateOptions
    43  		wantErr string
    44  	}{
    45  		{
    46  			name:  "only tag name",
    47  			args:  "v1.2.3",
    48  			isTTY: true,
    49  			want: CreateOptions{
    50  				TagName:      "v1.2.3",
    51  				Target:       "",
    52  				Name:         "",
    53  				Body:         "",
    54  				BodyProvided: false,
    55  				Draft:        false,
    56  				Prerelease:   false,
    57  				RepoOverride: "",
    58  				Concurrency:  5,
    59  				Assets:       []*shared.AssetForUpload(nil),
    60  			},
    61  		},
    62  		{
    63  			name:  "asset files",
    64  			args:  fmt.Sprintf("v1.2.3 '%s' '%s#Linux build'", af1.Name(), af2.Name()),
    65  			isTTY: true,
    66  			want: CreateOptions{
    67  				TagName:      "v1.2.3",
    68  				Target:       "",
    69  				Name:         "",
    70  				Body:         "",
    71  				BodyProvided: false,
    72  				Draft:        false,
    73  				Prerelease:   false,
    74  				RepoOverride: "",
    75  				Concurrency:  5,
    76  				Assets: []*shared.AssetForUpload{
    77  					{
    78  						Name:  "windows.zip",
    79  						Label: "",
    80  					},
    81  					{
    82  						Name:  "linux.tgz",
    83  						Label: "Linux build",
    84  					},
    85  				},
    86  			},
    87  		},
    88  		{
    89  			name:  "provide title and body",
    90  			args:  "v1.2.3 -t mytitle -n mynotes",
    91  			isTTY: true,
    92  			want: CreateOptions{
    93  				TagName:      "v1.2.3",
    94  				Target:       "",
    95  				Name:         "mytitle",
    96  				Body:         "mynotes",
    97  				BodyProvided: true,
    98  				Draft:        false,
    99  				Prerelease:   false,
   100  				RepoOverride: "",
   101  				Concurrency:  5,
   102  				Assets:       []*shared.AssetForUpload(nil),
   103  			},
   104  		},
   105  		{
   106  			name:  "notes from file",
   107  			args:  fmt.Sprintf(`v1.2.3 -F '%s'`, tf.Name()),
   108  			isTTY: true,
   109  			want: CreateOptions{
   110  				TagName:      "v1.2.3",
   111  				Target:       "",
   112  				Name:         "",
   113  				Body:         "MY NOTES",
   114  				BodyProvided: true,
   115  				Draft:        false,
   116  				Prerelease:   false,
   117  				RepoOverride: "",
   118  				Concurrency:  5,
   119  				Assets:       []*shared.AssetForUpload(nil),
   120  			},
   121  		},
   122  		{
   123  			name:  "notes from stdin",
   124  			args:  "v1.2.3 -F -",
   125  			isTTY: true,
   126  			stdin: "MY NOTES",
   127  			want: CreateOptions{
   128  				TagName:      "v1.2.3",
   129  				Target:       "",
   130  				Name:         "",
   131  				Body:         "MY NOTES",
   132  				BodyProvided: true,
   133  				Draft:        false,
   134  				Prerelease:   false,
   135  				RepoOverride: "",
   136  				Concurrency:  5,
   137  				Assets:       []*shared.AssetForUpload(nil),
   138  			},
   139  		},
   140  		{
   141  			name:  "set draft and prerelease",
   142  			args:  "v1.2.3 -d -p",
   143  			isTTY: true,
   144  			want: CreateOptions{
   145  				TagName:      "v1.2.3",
   146  				Target:       "",
   147  				Name:         "",
   148  				Body:         "",
   149  				BodyProvided: false,
   150  				Draft:        true,
   151  				Prerelease:   true,
   152  				RepoOverride: "",
   153  				Concurrency:  5,
   154  				Assets:       []*shared.AssetForUpload(nil),
   155  			},
   156  		},
   157  		{
   158  			name:    "no arguments",
   159  			args:    "",
   160  			isTTY:   true,
   161  			wantErr: "could not create: no tag name provided",
   162  		},
   163  		{
   164  			name:  "discussion category",
   165  			args:  "v1.2.3 --discussion-category 'General'",
   166  			isTTY: true,
   167  			want: CreateOptions{
   168  				TagName:            "v1.2.3",
   169  				Target:             "",
   170  				Name:               "",
   171  				Body:               "",
   172  				BodyProvided:       false,
   173  				Draft:              false,
   174  				Prerelease:         false,
   175  				RepoOverride:       "",
   176  				Concurrency:        5,
   177  				Assets:             []*shared.AssetForUpload(nil),
   178  				DiscussionCategory: "General",
   179  			},
   180  		},
   181  		{
   182  			name:    "discussion category for draft release",
   183  			args:    "v1.2.3 -d --discussion-category 'General'",
   184  			isTTY:   true,
   185  			wantErr: "Discussions for draft releases not supported",
   186  		},
   187  	}
   188  	for _, tt := range tests {
   189  		t.Run(tt.name, func(t *testing.T) {
   190  			io, stdin, _, _ := iostreams.Test()
   191  			if tt.stdin == "" {
   192  				io.SetStdinTTY(tt.isTTY)
   193  			} else {
   194  				io.SetStdinTTY(false)
   195  				fmt.Fprint(stdin, tt.stdin)
   196  			}
   197  			io.SetStdoutTTY(tt.isTTY)
   198  			io.SetStderrTTY(tt.isTTY)
   199  
   200  			f := &cmdutil.Factory{
   201  				IOStreams: io,
   202  			}
   203  
   204  			var opts *CreateOptions
   205  			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
   206  				opts = o
   207  				return nil
   208  			})
   209  			cmd.PersistentFlags().StringP("repo", "R", "", "")
   210  
   211  			argv, err := shlex.Split(tt.args)
   212  			require.NoError(t, err)
   213  			cmd.SetArgs(argv)
   214  
   215  			cmd.SetIn(&bytes.Buffer{})
   216  			cmd.SetOut(ioutil.Discard)
   217  			cmd.SetErr(ioutil.Discard)
   218  
   219  			_, err = cmd.ExecuteC()
   220  			if tt.wantErr != "" {
   221  				require.EqualError(t, err, tt.wantErr)
   222  				return
   223  			} else {
   224  				require.NoError(t, err)
   225  			}
   226  
   227  			assert.Equal(t, tt.want.TagName, opts.TagName)
   228  			assert.Equal(t, tt.want.Target, opts.Target)
   229  			assert.Equal(t, tt.want.Name, opts.Name)
   230  			assert.Equal(t, tt.want.Body, opts.Body)
   231  			assert.Equal(t, tt.want.BodyProvided, opts.BodyProvided)
   232  			assert.Equal(t, tt.want.Draft, opts.Draft)
   233  			assert.Equal(t, tt.want.Prerelease, opts.Prerelease)
   234  			assert.Equal(t, tt.want.Concurrency, opts.Concurrency)
   235  			assert.Equal(t, tt.want.RepoOverride, opts.RepoOverride)
   236  			assert.Equal(t, tt.want.DiscussionCategory, opts.DiscussionCategory)
   237  
   238  			require.Equal(t, len(tt.want.Assets), len(opts.Assets))
   239  			for i := range tt.want.Assets {
   240  				assert.Equal(t, tt.want.Assets[i].Name, opts.Assets[i].Name)
   241  				assert.Equal(t, tt.want.Assets[i].Label, opts.Assets[i].Label)
   242  			}
   243  		})
   244  	}
   245  }
   246  
   247  func Test_createRun(t *testing.T) {
   248  	tests := []struct {
   249  		name       string
   250  		isTTY      bool
   251  		opts       CreateOptions
   252  		wantParams interface{}
   253  		wantErr    string
   254  		wantStdout string
   255  		wantStderr string
   256  	}{
   257  		{
   258  			name:  "create a release",
   259  			isTTY: true,
   260  			opts: CreateOptions{
   261  				TagName:      "v1.2.3",
   262  				Name:         "The Big 1.2",
   263  				Body:         "* Fixed bugs",
   264  				BodyProvided: true,
   265  				Target:       "",
   266  			},
   267  			wantParams: map[string]interface{}{
   268  				"tag_name":   "v1.2.3",
   269  				"name":       "The Big 1.2",
   270  				"body":       "* Fixed bugs",
   271  				"draft":      false,
   272  				"prerelease": false,
   273  			},
   274  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   275  			wantStderr: ``,
   276  		},
   277  		{
   278  			name:  "with discussion category",
   279  			isTTY: true,
   280  			opts: CreateOptions{
   281  				TagName:            "v1.2.3",
   282  				Name:               "The Big 1.2",
   283  				Body:               "* Fixed bugs",
   284  				BodyProvided:       true,
   285  				Target:             "",
   286  				DiscussionCategory: "General",
   287  			},
   288  			wantParams: map[string]interface{}{
   289  				"tag_name":                 "v1.2.3",
   290  				"name":                     "The Big 1.2",
   291  				"body":                     "* Fixed bugs",
   292  				"draft":                    false,
   293  				"prerelease":               false,
   294  				"discussion_category_name": "General",
   295  			},
   296  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   297  			wantStderr: ``,
   298  		},
   299  		{
   300  			name:  "with target commitish",
   301  			isTTY: true,
   302  			opts: CreateOptions{
   303  				TagName:      "v1.2.3",
   304  				Name:         "",
   305  				Body:         "",
   306  				BodyProvided: true,
   307  				Target:       "main",
   308  			},
   309  			wantParams: map[string]interface{}{
   310  				"tag_name":         "v1.2.3",
   311  				"name":             "",
   312  				"body":             "",
   313  				"draft":            false,
   314  				"prerelease":       false,
   315  				"target_commitish": "main",
   316  			},
   317  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   318  			wantStderr: ``,
   319  		},
   320  		{
   321  			name:  "as draft",
   322  			isTTY: true,
   323  			opts: CreateOptions{
   324  				TagName:      "v1.2.3",
   325  				Name:         "",
   326  				Body:         "",
   327  				BodyProvided: true,
   328  				Draft:        true,
   329  				Target:       "",
   330  			},
   331  			wantParams: map[string]interface{}{
   332  				"tag_name":   "v1.2.3",
   333  				"name":       "",
   334  				"body":       "",
   335  				"draft":      true,
   336  				"prerelease": false,
   337  			},
   338  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
   339  			wantStderr: ``,
   340  		},
   341  		{
   342  			name:  "discussion category for draft release",
   343  			isTTY: true,
   344  			opts: CreateOptions{
   345  				TagName:            "v1.2.3",
   346  				Name:               "",
   347  				Body:               "",
   348  				BodyProvided:       true,
   349  				Draft:              true,
   350  				Target:             "",
   351  				DiscussionCategory: "general",
   352  			},
   353  			wantParams: map[string]interface{}{
   354  				"tag_name":                 "v1.2.3",
   355  				"name":                     "",
   356  				"body":                     "",
   357  				"draft":                    true,
   358  				"prerelease":               false,
   359  				"discussion_category_name": "general",
   360  			},
   361  			wantErr:    "X Discussions not supported with draft releases",
   362  			wantStdout: "",
   363  		},
   364  		{
   365  			name:  "publish after uploading files",
   366  			isTTY: true,
   367  			opts: CreateOptions{
   368  				TagName:      "v1.2.3",
   369  				Name:         "",
   370  				Body:         "",
   371  				BodyProvided: true,
   372  				Draft:        false,
   373  				Target:       "",
   374  				Assets: []*shared.AssetForUpload{
   375  					{
   376  						Name: "ball.tgz",
   377  						Open: func() (io.ReadCloser, error) {
   378  							return ioutil.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
   379  						},
   380  					},
   381  				},
   382  				Concurrency: 1,
   383  			},
   384  			wantParams: map[string]interface{}{
   385  				"tag_name":   "v1.2.3",
   386  				"name":       "",
   387  				"body":       "",
   388  				"draft":      true,
   389  				"prerelease": false,
   390  			},
   391  			wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
   392  			wantStderr: ``,
   393  		},
   394  	}
   395  	for _, tt := range tests {
   396  		t.Run(tt.name, func(t *testing.T) {
   397  			io, _, stdout, stderr := iostreams.Test()
   398  			io.SetStdoutTTY(tt.isTTY)
   399  			io.SetStdinTTY(tt.isTTY)
   400  			io.SetStderrTTY(tt.isTTY)
   401  
   402  			fakeHTTP := &httpmock.Registry{}
   403  			fakeHTTP.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
   404  				"url": "https://api.github.com/releases/123",
   405  				"upload_url": "https://api.github.com/assets/upload",
   406  				"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
   407  			}`))
   408  			fakeHTTP.Register(httpmock.REST("POST", "assets/upload"), httpmock.StatusStringResponse(201, `{}`))
   409  			fakeHTTP.Register(httpmock.REST("PATCH", "releases/123"), httpmock.StatusStringResponse(201, `{
   410  				"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
   411  			}`))
   412  
   413  			tt.opts.IO = io
   414  			tt.opts.HttpClient = func() (*http.Client, error) {
   415  				return &http.Client{Transport: fakeHTTP}, nil
   416  			}
   417  			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
   418  				return ghrepo.FromFullName("OWNER/REPO")
   419  			}
   420  
   421  			err := createRun(&tt.opts)
   422  			if tt.wantErr != "" {
   423  				require.EqualError(t, err, tt.wantErr)
   424  				return
   425  			} else {
   426  				require.NoError(t, err)
   427  			}
   428  
   429  			bb, err := ioutil.ReadAll(fakeHTTP.Requests[0].Body)
   430  			require.NoError(t, err)
   431  			var params interface{}
   432  			err = json.Unmarshal(bb, &params)
   433  			require.NoError(t, err)
   434  			assert.Equal(t, tt.wantParams, params)
   435  
   436  			if len(tt.opts.Assets) > 0 {
   437  				q := fakeHTTP.Requests[1].URL.Query()
   438  				assert.Equal(t, tt.opts.Assets[0].Name, q.Get("name"))
   439  				assert.Equal(t, tt.opts.Assets[0].Label, q.Get("label"))
   440  
   441  				bb, err := ioutil.ReadAll(fakeHTTP.Requests[2].Body)
   442  				require.NoError(t, err)
   443  				var updateParams interface{}
   444  				err = json.Unmarshal(bb, &updateParams)
   445  				require.NoError(t, err)
   446  				assert.Equal(t, map[string]interface{}{"draft": false}, updateParams)
   447  			}
   448  
   449  			assert.Equal(t, tt.wantStdout, stdout.String())
   450  			assert.Equal(t, tt.wantStderr, stderr.String())
   451  		})
   452  	}
   453  }