1package diff
2
3import (
4	"bytes"
5	"fmt"
6	"io/ioutil"
7	"net/http"
8	"strings"
9	"testing"
10
11	"github.com/cli/cli/v2/api"
12	"github.com/cli/cli/v2/internal/ghrepo"
13	"github.com/cli/cli/v2/pkg/cmd/pr/shared"
14	"github.com/cli/cli/v2/pkg/cmdutil"
15	"github.com/cli/cli/v2/pkg/httpmock"
16	"github.com/cli/cli/v2/pkg/iostreams"
17	"github.com/google/go-cmp/cmp"
18	"github.com/google/shlex"
19	"github.com/stretchr/testify/assert"
20	"github.com/stretchr/testify/require"
21)
22
23func Test_NewCmdDiff(t *testing.T) {
24	tests := []struct {
25		name    string
26		args    string
27		isTTY   bool
28		want    DiffOptions
29		wantErr string
30	}{
31		{
32			name:  "number argument",
33			args:  "123",
34			isTTY: true,
35			want: DiffOptions{
36				SelectorArg: "123",
37				UseColor:    true,
38			},
39		},
40		{
41			name:  "no argument",
42			args:  "",
43			isTTY: true,
44			want: DiffOptions{
45				SelectorArg: "",
46				UseColor:    true,
47			},
48		},
49		{
50			name:  "no color when redirected",
51			args:  "",
52			isTTY: false,
53			want: DiffOptions{
54				SelectorArg: "",
55				UseColor:    false,
56			},
57		},
58		{
59			name:  "force color",
60			args:  "--color always",
61			isTTY: false,
62			want: DiffOptions{
63				SelectorArg: "",
64				UseColor:    true,
65			},
66		},
67		{
68			name:  "disable color",
69			args:  "--color never",
70			isTTY: true,
71			want: DiffOptions{
72				SelectorArg: "",
73				UseColor:    false,
74			},
75		},
76		{
77			name:    "no argument with --repo override",
78			args:    "-R owner/repo",
79			isTTY:   true,
80			wantErr: "argument required when using the `--repo` flag",
81		},
82		{
83			name:    "invalid --color argument",
84			args:    "--color doublerainbow",
85			isTTY:   true,
86			wantErr: "the value for `--color` must be one of \"auto\", \"always\", or \"never\"",
87		},
88	}
89	for _, tt := range tests {
90		t.Run(tt.name, func(t *testing.T) {
91			io, _, _, _ := iostreams.Test()
92			io.SetStdoutTTY(tt.isTTY)
93			io.SetStdinTTY(tt.isTTY)
94			io.SetStderrTTY(tt.isTTY)
95			io.SetColorEnabled(tt.isTTY)
96
97			f := &cmdutil.Factory{
98				IOStreams: io,
99			}
100
101			var opts *DiffOptions
102			cmd := NewCmdDiff(f, func(o *DiffOptions) error {
103				opts = o
104				return nil
105			})
106			cmd.PersistentFlags().StringP("repo", "R", "", "")
107
108			argv, err := shlex.Split(tt.args)
109			require.NoError(t, err)
110			cmd.SetArgs(argv)
111
112			cmd.SetIn(&bytes.Buffer{})
113			cmd.SetOut(ioutil.Discard)
114			cmd.SetErr(ioutil.Discard)
115
116			_, err = cmd.ExecuteC()
117			if tt.wantErr != "" {
118				require.EqualError(t, err, tt.wantErr)
119				return
120			} else {
121				require.NoError(t, err)
122			}
123
124			assert.Equal(t, tt.want.SelectorArg, opts.SelectorArg)
125			assert.Equal(t, tt.want.UseColor, opts.UseColor)
126		})
127	}
128}
129
130func Test_diffRun(t *testing.T) {
131	pr := &api.PullRequest{Number: 123}
132
133	tests := []struct {
134		name       string
135		opts       DiffOptions
136		rawDiff    string
137		wantAccept string
138		wantStdout string
139	}{
140		{
141			name: "no color",
142			opts: DiffOptions{
143				SelectorArg: "123",
144				UseColor:    false,
145				Patch:       false,
146			},
147			rawDiff:    fmt.Sprintf(testDiff, "", "", "", ""),
148			wantAccept: "application/vnd.github.v3.diff",
149			wantStdout: fmt.Sprintf(testDiff, "", "", "", ""),
150		},
151		{
152			name: "with color",
153			opts: DiffOptions{
154				SelectorArg: "123",
155				UseColor:    true,
156				Patch:       false,
157			},
158			rawDiff:    fmt.Sprintf(testDiff, "", "", "", ""),
159			wantAccept: "application/vnd.github.v3.diff",
160			wantStdout: fmt.Sprintf(testDiff, "\x1b[m", "\x1b[1;38m", "\x1b[32m", "\x1b[31m"),
161		},
162		{
163			name: "patch format",
164			opts: DiffOptions{
165				SelectorArg: "123",
166				UseColor:    false,
167				Patch:       true,
168			},
169			rawDiff:    fmt.Sprintf(testDiff, "", "", "", ""),
170			wantAccept: "application/vnd.github.v3.patch",
171			wantStdout: fmt.Sprintf(testDiff, "", "", "", ""),
172		},
173	}
174	for _, tt := range tests {
175		t.Run(tt.name, func(t *testing.T) {
176			httpReg := &httpmock.Registry{}
177			defer httpReg.Verify(t)
178
179			var gotAccept string
180			httpReg.Register(
181				httpmock.REST("GET", "repos/OWNER/REPO/pulls/123"),
182				func(req *http.Request) (*http.Response, error) {
183					gotAccept = req.Header.Get("Accept")
184					return &http.Response{
185						StatusCode: 200,
186						Request:    req,
187						Body:       ioutil.NopCloser(strings.NewReader(tt.rawDiff)),
188					}, nil
189				})
190
191			opts := tt.opts
192			opts.HttpClient = func() (*http.Client, error) {
193				return &http.Client{Transport: httpReg}, nil
194			}
195
196			io, _, stdout, stderr := iostreams.Test()
197			opts.IO = io
198
199			finder := shared.NewMockFinder("123", pr, ghrepo.New("OWNER", "REPO"))
200			finder.ExpectFields([]string{"number"})
201			opts.Finder = finder
202
203			if err := diffRun(&opts); err != nil {
204				t.Fatalf("unexpected error: %s", err)
205			}
206			if diff := cmp.Diff(tt.wantStdout, stdout.String()); diff != "" {
207				t.Errorf("command output did not match:\n%s", diff)
208			}
209			if stderr.String() != "" {
210				t.Errorf("unexpected stderr output: %s", stderr.String())
211			}
212			if gotAccept != tt.wantAccept {
213				t.Errorf("unexpected Accept header: %s", gotAccept)
214			}
215		})
216	}
217}
218
219const testDiff = `%[2]sdiff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml%[1]s
220%[2]sindex 73974448..b7fc0154 100644%[1]s
221%[2]s--- a/.github/workflows/releases.yml%[1]s
222%[2]s+++ b/.github/workflows/releases.yml%[1]s
223@@ -44,6 +44,11 @@ jobs:
224           token: ${{secrets.SITE_GITHUB_TOKEN}}
225       - name: Publish documentation site
226         if: "!contains(github.ref, '-')" # skip prereleases
227%[3]s+        env:%[1]s
228%[3]s+          GIT_COMMITTER_NAME: cli automation%[1]s
229%[3]s+          GIT_AUTHOR_NAME: cli automation%[1]s
230%[3]s+          GIT_COMMITTER_EMAIL: noreply@github.com%[1]s
231%[3]s+          GIT_AUTHOR_EMAIL: noreply@github.com%[1]s
232         run: make site-publish
233       - name: Move project cards
234         if: "!contains(github.ref, '-')" # skip prereleases
235%[2]sdiff --git a/Makefile b/Makefile%[1]s
236%[2]sindex f2b4805c..3d7bd0f9 100644%[1]s
237%[2]s--- a/Makefile%[1]s
238%[2]s+++ b/Makefile%[1]s
239@@ -22,8 +22,8 @@ test:
240 	go test ./...
241 .PHONY: test
242
243%[4]s-site:%[1]s
244%[4]s-	git clone https://github.com/github/cli.github.com.git "$@"%[1]s
245%[3]s+site: bin/gh%[1]s
246%[3]s+	bin/gh repo clone github/cli.github.com "$@"%[1]s
247
248 site-docs: site
249 	git -C site pull
250`
251
252func Test_colorDiffLines(t *testing.T) {
253	inputs := []struct {
254		input, output string
255	}{
256		{
257			input:  "",
258			output: "",
259		},
260		{
261			input:  "\n",
262			output: "\n",
263		},
264		{
265			input:  "foo\nbar\nbaz\n",
266			output: "foo\nbar\nbaz\n",
267		},
268		{
269			input:  "foo\nbar\nbaz",
270			output: "foo\nbar\nbaz\n",
271		},
272		{
273			input: fmt.Sprintf("+foo\n-b%sr\n+++ baz\n", strings.Repeat("a", 2*lineBufferSize)),
274			output: fmt.Sprintf(
275				"%[4]s+foo%[2]s\n%[5]s-b%[1]sr%[2]s\n%[3]s+++ baz%[2]s\n",
276				strings.Repeat("a", 2*lineBufferSize),
277				"\x1b[m",
278				"\x1b[1;38m",
279				"\x1b[32m",
280				"\x1b[31m",
281			),
282		},
283	}
284	for _, tt := range inputs {
285		buf := bytes.Buffer{}
286		if err := colorDiffLines(&buf, strings.NewReader(tt.input)); err != nil {
287			t.Fatalf("unexpected error: %s", err)
288		}
289		if got := buf.String(); got != tt.output {
290			t.Errorf("expected: %q, got: %q", tt.output, got)
291		}
292	}
293}
294