1package config
2
3import (
4	"bytes"
5	"fmt"
6	"io/ioutil"
7	"os"
8	"path/filepath"
9	"runtime"
10	"testing"
11
12	"github.com/stretchr/testify/assert"
13	"gopkg.in/yaml.v3"
14)
15
16func Test_parseConfig(t *testing.T) {
17	defer stubConfig(`---
18hosts:
19  github.com:
20    user: monalisa
21    oauth_token: OTOKEN
22`, "")()
23	config, err := parseConfig("config.yml")
24	assert.NoError(t, err)
25	user, err := config.Get("github.com", "user")
26	assert.NoError(t, err)
27	assert.Equal(t, "monalisa", user)
28	token, err := config.Get("github.com", "oauth_token")
29	assert.NoError(t, err)
30	assert.Equal(t, "OTOKEN", token)
31}
32
33func Test_parseConfig_multipleHosts(t *testing.T) {
34	defer stubConfig(`---
35hosts:
36  example.com:
37    user: wronguser
38    oauth_token: NOTTHIS
39  github.com:
40    user: monalisa
41    oauth_token: OTOKEN
42`, "")()
43	config, err := parseConfig("config.yml")
44	assert.NoError(t, err)
45	user, err := config.Get("github.com", "user")
46	assert.NoError(t, err)
47	assert.Equal(t, "monalisa", user)
48	token, err := config.Get("github.com", "oauth_token")
49	assert.NoError(t, err)
50	assert.Equal(t, "OTOKEN", token)
51}
52
53func Test_parseConfig_hostsFile(t *testing.T) {
54	defer stubConfig("", `---
55github.com:
56  user: monalisa
57  oauth_token: OTOKEN
58`)()
59	config, err := parseConfig("config.yml")
60	assert.NoError(t, err)
61	user, err := config.Get("github.com", "user")
62	assert.NoError(t, err)
63	assert.Equal(t, "monalisa", user)
64	token, err := config.Get("github.com", "oauth_token")
65	assert.NoError(t, err)
66	assert.Equal(t, "OTOKEN", token)
67}
68
69func Test_parseConfig_hostFallback(t *testing.T) {
70	defer stubConfig(`---
71git_protocol: ssh
72`, `---
73github.com:
74    user: monalisa
75    oauth_token: OTOKEN
76example.com:
77    user: wronguser
78    oauth_token: NOTTHIS
79    git_protocol: https
80`)()
81	config, err := parseConfig("config.yml")
82	assert.NoError(t, err)
83	val, err := config.Get("example.com", "git_protocol")
84	assert.NoError(t, err)
85	assert.Equal(t, "https", val)
86	val, err = config.Get("github.com", "git_protocol")
87	assert.NoError(t, err)
88	assert.Equal(t, "ssh", val)
89	val, err = config.Get("nonexistent.io", "git_protocol")
90	assert.NoError(t, err)
91	assert.Equal(t, "ssh", val)
92}
93
94func Test_parseConfig_migrateConfig(t *testing.T) {
95	defer stubConfig(`---
96github.com:
97  - user: keiyuri
98    oauth_token: 123456
99`, "")()
100
101	mainBuf := bytes.Buffer{}
102	hostsBuf := bytes.Buffer{}
103	defer StubWriteConfig(&mainBuf, &hostsBuf)()
104	defer StubBackupConfig()()
105
106	_, err := parseConfig("config.yml")
107	assert.NoError(t, err)
108
109	expectedHosts := `github.com:
110    user: keiyuri
111    oauth_token: "123456"
112`
113
114	assert.Equal(t, expectedHosts, hostsBuf.String())
115	assert.NotContains(t, mainBuf.String(), "github.com")
116	assert.NotContains(t, mainBuf.String(), "oauth_token")
117}
118
119func Test_parseConfigFile(t *testing.T) {
120	tests := []struct {
121		contents string
122		wantsErr bool
123	}{
124		{
125			contents: "",
126			wantsErr: true,
127		},
128		{
129			contents: " ",
130			wantsErr: false,
131		},
132		{
133			contents: "\n",
134			wantsErr: false,
135		},
136	}
137
138	for _, tt := range tests {
139		t.Run(fmt.Sprintf("contents: %q", tt.contents), func(t *testing.T) {
140			defer stubConfig(tt.contents, "")()
141			_, yamlRoot, err := parseConfigFile("config.yml")
142			if tt.wantsErr != (err != nil) {
143				t.Fatalf("got error: %v", err)
144			}
145			if tt.wantsErr {
146				return
147			}
148			assert.Equal(t, yaml.MappingNode, yamlRoot.Content[0].Kind)
149			assert.Equal(t, 0, len(yamlRoot.Content[0].Content))
150		})
151	}
152}
153
154func Test_ConfigDir(t *testing.T) {
155	tempDir := t.TempDir()
156
157	tests := []struct {
158		name        string
159		onlyWindows bool
160		env         map[string]string
161		output      string
162	}{
163		{
164			name: "HOME/USERPROFILE specified",
165			env: map[string]string{
166				"GH_CONFIG_DIR":   "",
167				"XDG_CONFIG_HOME": "",
168				"AppData":         "",
169				"USERPROFILE":     tempDir,
170				"HOME":            tempDir,
171			},
172			output: filepath.Join(tempDir, ".config", "gh"),
173		},
174		{
175			name: "GH_CONFIG_DIR specified",
176			env: map[string]string{
177				"GH_CONFIG_DIR": filepath.Join(tempDir, "gh_config_dir"),
178			},
179			output: filepath.Join(tempDir, "gh_config_dir"),
180		},
181		{
182			name: "XDG_CONFIG_HOME specified",
183			env: map[string]string{
184				"XDG_CONFIG_HOME": tempDir,
185			},
186			output: filepath.Join(tempDir, "gh"),
187		},
188		{
189			name: "GH_CONFIG_DIR and XDG_CONFIG_HOME specified",
190			env: map[string]string{
191				"GH_CONFIG_DIR":   filepath.Join(tempDir, "gh_config_dir"),
192				"XDG_CONFIG_HOME": tempDir,
193			},
194			output: filepath.Join(tempDir, "gh_config_dir"),
195		},
196		{
197			name:        "AppData specified",
198			onlyWindows: true,
199			env: map[string]string{
200				"AppData": tempDir,
201			},
202			output: filepath.Join(tempDir, "GitHub CLI"),
203		},
204		{
205			name:        "GH_CONFIG_DIR and AppData specified",
206			onlyWindows: true,
207			env: map[string]string{
208				"GH_CONFIG_DIR": filepath.Join(tempDir, "gh_config_dir"),
209				"AppData":       tempDir,
210			},
211			output: filepath.Join(tempDir, "gh_config_dir"),
212		},
213		{
214			name:        "XDG_CONFIG_HOME and AppData specified",
215			onlyWindows: true,
216			env: map[string]string{
217				"XDG_CONFIG_HOME": tempDir,
218				"AppData":         tempDir,
219			},
220			output: filepath.Join(tempDir, "gh"),
221		},
222	}
223
224	for _, tt := range tests {
225		if tt.onlyWindows && runtime.GOOS != "windows" {
226			continue
227		}
228		t.Run(tt.name, func(t *testing.T) {
229			if tt.env != nil {
230				for k, v := range tt.env {
231					old := os.Getenv(k)
232					os.Setenv(k, v)
233					defer os.Setenv(k, old)
234				}
235			}
236
237			// Create directory to skip auto migration code
238			// which gets run when target directory does not exist
239			_ = os.MkdirAll(tt.output, 0755)
240
241			assert.Equal(t, tt.output, ConfigDir())
242		})
243	}
244}
245
246func Test_configFile_Write_toDisk(t *testing.T) {
247	configDir := filepath.Join(t.TempDir(), ".config", "gh")
248	_ = os.MkdirAll(configDir, 0755)
249	os.Setenv(GH_CONFIG_DIR, configDir)
250	defer os.Unsetenv(GH_CONFIG_DIR)
251
252	cfg := NewFromString(`pager: less`)
253	err := cfg.Write()
254	if err != nil {
255		t.Fatal(err)
256	}
257
258	expectedConfig := "pager: less\n"
259	if configBytes, err := ioutil.ReadFile(filepath.Join(configDir, "config.yml")); err != nil {
260		t.Error(err)
261	} else if string(configBytes) != expectedConfig {
262		t.Errorf("expected config.yml %q, got %q", expectedConfig, string(configBytes))
263	}
264
265	if configBytes, err := ioutil.ReadFile(filepath.Join(configDir, "hosts.yml")); err != nil {
266		t.Error(err)
267	} else if string(configBytes) != "" {
268		t.Errorf("unexpected hosts.yml: %q", string(configBytes))
269	}
270}
271
272func Test_autoMigrateConfigDir_noMigration_notExist(t *testing.T) {
273	homeDir := t.TempDir()
274	migrateDir := t.TempDir()
275
276	homeEnvVar := "HOME"
277	if runtime.GOOS == "windows" {
278		homeEnvVar = "USERPROFILE"
279	}
280	old := os.Getenv(homeEnvVar)
281	os.Setenv(homeEnvVar, homeDir)
282	defer os.Setenv(homeEnvVar, old)
283
284	err := autoMigrateConfigDir(migrateDir)
285	assert.Equal(t, errNotExist, err)
286
287	files, err := ioutil.ReadDir(migrateDir)
288	assert.NoError(t, err)
289	assert.Equal(t, 0, len(files))
290}
291
292func Test_autoMigrateConfigDir_noMigration_samePath(t *testing.T) {
293	homeDir := t.TempDir()
294	migrateDir := filepath.Join(homeDir, ".config", "gh")
295	err := os.MkdirAll(migrateDir, 0755)
296	assert.NoError(t, err)
297
298	homeEnvVar := "HOME"
299	if runtime.GOOS == "windows" {
300		homeEnvVar = "USERPROFILE"
301	}
302	old := os.Getenv(homeEnvVar)
303	os.Setenv(homeEnvVar, homeDir)
304	defer os.Setenv(homeEnvVar, old)
305
306	err = autoMigrateConfigDir(migrateDir)
307	assert.Equal(t, errSamePath, err)
308
309	files, err := ioutil.ReadDir(migrateDir)
310	assert.NoError(t, err)
311	assert.Equal(t, 0, len(files))
312}
313
314func Test_autoMigrateConfigDir_migration(t *testing.T) {
315	homeDir := t.TempDir()
316	migrateDir := t.TempDir()
317	homeConfigDir := filepath.Join(homeDir, ".config", "gh")
318	migrateConfigDir := filepath.Join(migrateDir, ".config", "gh")
319
320	homeEnvVar := "HOME"
321	if runtime.GOOS == "windows" {
322		homeEnvVar = "USERPROFILE"
323	}
324	old := os.Getenv(homeEnvVar)
325	os.Setenv(homeEnvVar, homeDir)
326	defer os.Setenv(homeEnvVar, old)
327
328	err := os.MkdirAll(homeConfigDir, 0755)
329	assert.NoError(t, err)
330	f, err := ioutil.TempFile(homeConfigDir, "")
331	assert.NoError(t, err)
332	f.Close()
333
334	err = autoMigrateConfigDir(migrateConfigDir)
335	assert.NoError(t, err)
336
337	_, err = ioutil.ReadDir(homeConfigDir)
338	assert.True(t, os.IsNotExist(err))
339
340	files, err := ioutil.ReadDir(migrateConfigDir)
341	assert.NoError(t, err)
342	assert.Equal(t, 1, len(files))
343}
344
345func Test_StateDir(t *testing.T) {
346	tempDir := t.TempDir()
347
348	tests := []struct {
349		name        string
350		onlyWindows bool
351		env         map[string]string
352		output      string
353	}{
354		{
355			name: "HOME/USERPROFILE specified",
356			env: map[string]string{
357				"XDG_STATE_HOME":  "",
358				"GH_CONFIG_DIR":   "",
359				"XDG_CONFIG_HOME": "",
360				"LocalAppData":    "",
361				"USERPROFILE":     tempDir,
362				"HOME":            tempDir,
363			},
364			output: filepath.Join(tempDir, ".local", "state", "gh"),
365		},
366		{
367			name: "XDG_STATE_HOME specified",
368			env: map[string]string{
369				"XDG_STATE_HOME": tempDir,
370			},
371			output: filepath.Join(tempDir, "gh"),
372		},
373		{
374			name:        "LocalAppData specified",
375			onlyWindows: true,
376			env: map[string]string{
377				"LocalAppData": tempDir,
378			},
379			output: filepath.Join(tempDir, "GitHub CLI"),
380		},
381		{
382			name:        "XDG_STATE_HOME and LocalAppData specified",
383			onlyWindows: true,
384			env: map[string]string{
385				"XDG_STATE_HOME": tempDir,
386				"LocalAppData":   tempDir,
387			},
388			output: filepath.Join(tempDir, "gh"),
389		},
390	}
391
392	for _, tt := range tests {
393		if tt.onlyWindows && runtime.GOOS != "windows" {
394			continue
395		}
396		t.Run(tt.name, func(t *testing.T) {
397			if tt.env != nil {
398				for k, v := range tt.env {
399					old := os.Getenv(k)
400					os.Setenv(k, v)
401					defer os.Setenv(k, old)
402				}
403			}
404
405			// Create directory to skip auto migration code
406			// which gets run when target directory does not exist
407			_ = os.MkdirAll(tt.output, 0755)
408
409			assert.Equal(t, tt.output, StateDir())
410		})
411	}
412}
413
414func Test_autoMigrateStateDir_noMigration_notExist(t *testing.T) {
415	homeDir := t.TempDir()
416	migrateDir := t.TempDir()
417
418	homeEnvVar := "HOME"
419	if runtime.GOOS == "windows" {
420		homeEnvVar = "USERPROFILE"
421	}
422	old := os.Getenv(homeEnvVar)
423	os.Setenv(homeEnvVar, homeDir)
424	defer os.Setenv(homeEnvVar, old)
425
426	err := autoMigrateStateDir(migrateDir)
427	assert.Equal(t, errNotExist, err)
428
429	files, err := ioutil.ReadDir(migrateDir)
430	assert.NoError(t, err)
431	assert.Equal(t, 0, len(files))
432}
433
434func Test_autoMigrateStateDir_noMigration_samePath(t *testing.T) {
435	homeDir := t.TempDir()
436	migrateDir := filepath.Join(homeDir, ".config", "gh")
437	err := os.MkdirAll(migrateDir, 0755)
438	assert.NoError(t, err)
439
440	homeEnvVar := "HOME"
441	if runtime.GOOS == "windows" {
442		homeEnvVar = "USERPROFILE"
443	}
444	old := os.Getenv(homeEnvVar)
445	os.Setenv(homeEnvVar, homeDir)
446	defer os.Setenv(homeEnvVar, old)
447
448	err = autoMigrateStateDir(migrateDir)
449	assert.Equal(t, errSamePath, err)
450
451	files, err := ioutil.ReadDir(migrateDir)
452	assert.NoError(t, err)
453	assert.Equal(t, 0, len(files))
454}
455
456func Test_autoMigrateStateDir_migration(t *testing.T) {
457	homeDir := t.TempDir()
458	migrateDir := t.TempDir()
459	homeConfigDir := filepath.Join(homeDir, ".config", "gh")
460	migrateStateDir := filepath.Join(migrateDir, ".local", "state", "gh")
461
462	homeEnvVar := "HOME"
463	if runtime.GOOS == "windows" {
464		homeEnvVar = "USERPROFILE"
465	}
466	old := os.Getenv(homeEnvVar)
467	os.Setenv(homeEnvVar, homeDir)
468	defer os.Setenv(homeEnvVar, old)
469
470	err := os.MkdirAll(homeConfigDir, 0755)
471	assert.NoError(t, err)
472	err = ioutil.WriteFile(filepath.Join(homeConfigDir, "state.yml"), nil, 0755)
473	assert.NoError(t, err)
474
475	err = autoMigrateStateDir(migrateStateDir)
476	assert.NoError(t, err)
477
478	files, err := ioutil.ReadDir(homeConfigDir)
479	assert.NoError(t, err)
480	assert.Equal(t, 0, len(files))
481
482	files, err = ioutil.ReadDir(migrateStateDir)
483	assert.NoError(t, err)
484	assert.Equal(t, 1, len(files))
485	assert.Equal(t, "state.yml", files[0].Name())
486}
487
488func Test_DataDir(t *testing.T) {
489	tempDir := t.TempDir()
490
491	tests := []struct {
492		name        string
493		onlyWindows bool
494		env         map[string]string
495		output      string
496	}{
497		{
498			name: "HOME/USERPROFILE specified",
499			env: map[string]string{
500				"XDG_DATA_HOME":   "",
501				"GH_CONFIG_DIR":   "",
502				"XDG_CONFIG_HOME": "",
503				"LocalAppData":    "",
504				"USERPROFILE":     tempDir,
505				"HOME":            tempDir,
506			},
507			output: filepath.Join(tempDir, ".local", "share", "gh"),
508		},
509		{
510			name: "XDG_DATA_HOME specified",
511			env: map[string]string{
512				"XDG_DATA_HOME": tempDir,
513			},
514			output: filepath.Join(tempDir, "gh"),
515		},
516		{
517			name:        "LocalAppData specified",
518			onlyWindows: true,
519			env: map[string]string{
520				"LocalAppData": tempDir,
521			},
522			output: filepath.Join(tempDir, "GitHub CLI"),
523		},
524		{
525			name:        "XDG_DATA_HOME and LocalAppData specified",
526			onlyWindows: true,
527			env: map[string]string{
528				"XDG_DATA_HOME": tempDir,
529				"LocalAppData":  tempDir,
530			},
531			output: filepath.Join(tempDir, "gh"),
532		},
533	}
534
535	for _, tt := range tests {
536		if tt.onlyWindows && runtime.GOOS != "windows" {
537			continue
538		}
539		t.Run(tt.name, func(t *testing.T) {
540			if tt.env != nil {
541				for k, v := range tt.env {
542					old := os.Getenv(k)
543					os.Setenv(k, v)
544					defer os.Setenv(k, old)
545				}
546			}
547
548			assert.Equal(t, tt.output, DataDir())
549		})
550	}
551}
552