1package cmd
2
3import (
4	"encoding/json"
5	"fmt"
6	"io/ioutil"
7	"net/http"
8	"net/http/httptest"
9	"os"
10	"path/filepath"
11	"strconv"
12	"testing"
13
14	"github.com/exercism/cli/config"
15	"github.com/exercism/cli/workspace"
16	"github.com/spf13/pflag"
17	"github.com/spf13/viper"
18	"github.com/stretchr/testify/assert"
19)
20
21func TestDownloadWithoutToken(t *testing.T) {
22	cfg := config.Config{
23		UserViperConfig: viper.New(),
24	}
25
26	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
27	if assert.Error(t, err) {
28		assert.Regexp(t, "Welcome to Exercism", err.Error())
29		// It uses the default base API url to infer the host
30		assert.Regexp(t, "exercism.io/my/settings", err.Error())
31	}
32}
33
34func TestDownloadWithoutWorkspace(t *testing.T) {
35	v := viper.New()
36	v.Set("token", "abc123")
37	cfg := config.Config{
38		UserViperConfig: v,
39	}
40
41	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
42	if assert.Error(t, err) {
43		assert.Regexp(t, "re-run the configure", err.Error())
44	}
45}
46
47func TestDownloadWithoutBaseURL(t *testing.T) {
48	v := viper.New()
49	v.Set("token", "abc123")
50	v.Set("workspace", "/home/whatever")
51	cfg := config.Config{
52		UserViperConfig: v,
53	}
54
55	err := runDownload(cfg, pflag.NewFlagSet("fake", pflag.PanicOnError), []string{})
56	if assert.Error(t, err) {
57		assert.Regexp(t, "re-run the configure", err.Error())
58	}
59}
60
61func TestDownloadWithoutFlags(t *testing.T) {
62	v := viper.New()
63	v.Set("token", "abc123")
64	v.Set("workspace", "/home/username")
65	v.Set("apibaseurl", "http://example.com")
66
67	cfg := config.Config{
68		UserViperConfig: v,
69	}
70
71	flags := pflag.NewFlagSet("fake", pflag.PanicOnError)
72	setupDownloadFlags(flags)
73
74	err := runDownload(cfg, flags, []string{})
75	if assert.Error(t, err) {
76		assert.Regexp(t, "need an --exercise name or a solution --uuid", err.Error())
77	}
78}
79
80func TestSolutionFile(t *testing.T) {
81	testCases := []struct {
82		name, file, expectedPath, expectedURL string
83	}{
84		{
85			name:         "filename with special character",
86			file:         "special-char-filename#.txt",
87			expectedPath: "special-char-filename#.txt",
88			expectedURL:  "http://www.example.com/special-char-filename%23.txt",
89		},
90		{
91			name:         "filename with leading slash",
92			file:         "/with-leading-slash.txt",
93			expectedPath: fmt.Sprintf("%cwith-leading-slash.txt", os.PathSeparator),
94			expectedURL:  "http://www.example.com//with-leading-slash.txt",
95		},
96		{
97			name:         "filename with leading backslash",
98			file:         "\\with-leading-backslash.txt",
99			expectedPath: fmt.Sprintf("%cwith-leading-backslash.txt", os.PathSeparator),
100			expectedURL:  "http://www.example.com/%5Cwith-leading-backslash.txt",
101		},
102		{
103			name:         "filename with backslashes in path",
104			file:         "\\backslashes\\in-path.txt",
105			expectedPath: fmt.Sprintf("%[1]cbackslashes%[1]cin-path.txt", os.PathSeparator),
106			expectedURL:  "http://www.example.com/%5Cbackslashes%5Cin-path.txt",
107		},
108		{
109			name:         "path with a numeric suffix",
110			file:         "/bogus-exercise-12345/numeric.txt",
111			expectedPath: fmt.Sprintf("%[1]cbogus-exercise-12345%[1]cnumeric.txt", os.PathSeparator),
112			expectedURL:  "http://www.example.com//bogus-exercise-12345/numeric.txt",
113		},
114	}
115
116	for _, tc := range testCases {
117		t.Run(tc.name, func(t *testing.T) {
118			sf := solutionFile{
119				path:    tc.file,
120				baseURL: "http://www.example.com/",
121			}
122
123			if sf.relativePath() != tc.expectedPath {
124				t.Fatalf("Expected path '%s', got '%s'", tc.expectedPath, sf.relativePath())
125			}
126
127			url, err := sf.url()
128			if err != nil {
129				t.Fatal(err)
130			}
131
132			if url != tc.expectedURL {
133				t.Fatalf("Expected URL '%s', got '%s'", tc.expectedURL, url)
134			}
135		})
136	}
137}
138
139func TestDownload(t *testing.T) {
140	co := newCapturedOutput()
141	co.override()
142	defer co.reset()
143
144	testCases := []struct {
145		requester   bool
146		expectedDir string
147		flags       map[string]string
148	}{
149		{
150			requester:   true,
151			expectedDir: "",
152			flags:       map[string]string{"exercise": "bogus-exercise"},
153		},
154		{
155			requester:   true,
156			expectedDir: "",
157			flags:       map[string]string{"uuid": "bogus-id"},
158		},
159		{
160			requester:   false,
161			expectedDir: filepath.Join("users", "alice"),
162			flags:       map[string]string{"uuid": "bogus-id"},
163		},
164		{
165			requester:   true,
166			expectedDir: filepath.Join("teams", "bogus-team"),
167			flags:       map[string]string{"exercise": "bogus-exercise", "track": "bogus-track", "team": "bogus-team"},
168		},
169	}
170
171	for _, tc := range testCases {
172		tmpDir, err := ioutil.TempDir("", "download-cmd")
173		defer os.RemoveAll(tmpDir)
174		assert.NoError(t, err)
175
176		ts := fakeDownloadServer(strconv.FormatBool(tc.requester), tc.flags["team"])
177		defer ts.Close()
178
179		v := viper.New()
180		v.Set("workspace", tmpDir)
181		v.Set("apibaseurl", ts.URL)
182		v.Set("token", "abc123")
183
184		cfg := config.Config{
185			UserViperConfig: v,
186		}
187		flags := pflag.NewFlagSet("fake", pflag.PanicOnError)
188		setupDownloadFlags(flags)
189		for name, value := range tc.flags {
190			flags.Set(name, value)
191		}
192
193		err = runDownload(cfg, flags, []string{})
194		assert.NoError(t, err)
195
196		targetDir := filepath.Join(tmpDir, tc.expectedDir)
197		assertDownloadedCorrectFiles(t, targetDir)
198
199		dir := filepath.Join(targetDir, "bogus-track", "bogus-exercise")
200		b, err := ioutil.ReadFile(workspace.NewExerciseFromDir(dir).MetadataFilepath())
201		assert.NoError(t, err)
202		var metadata workspace.ExerciseMetadata
203		err = json.Unmarshal(b, &metadata)
204		assert.NoError(t, err)
205
206		assert.Equal(t, "bogus-track", metadata.Track)
207		assert.Equal(t, "bogus-exercise", metadata.ExerciseSlug)
208		assert.Equal(t, tc.requester, metadata.IsRequester)
209	}
210}
211
212func fakeDownloadServer(requestor, teamSlug string) *httptest.Server {
213	mux := http.NewServeMux()
214	server := httptest.NewServer(mux)
215
216	mux.HandleFunc("/file-1.txt", func(w http.ResponseWriter, r *http.Request) {
217		fmt.Fprint(w, "this is file 1")
218	})
219
220	mux.HandleFunc("/subdir/file-2.txt", func(w http.ResponseWriter, r *http.Request) {
221		fmt.Fprint(w, "this is file 2")
222	})
223
224	mux.HandleFunc("/file-3.txt", func(w http.ResponseWriter, r *http.Request) {
225		fmt.Fprint(w, "")
226	})
227
228	mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) {
229		team := "null"
230		if teamSlug := r.FormValue("team_id"); teamSlug != "" {
231			team = fmt.Sprintf(`{"name": "Bogus Team", "slug": "%s"}`, teamSlug)
232		}
233		payloadBody := fmt.Sprintf(payloadTemplate, requestor, team, server.URL+"/")
234		fmt.Fprint(w, payloadBody)
235	})
236	mux.HandleFunc("/solutions/bogus-id", func(w http.ResponseWriter, r *http.Request) {
237		payloadBody := fmt.Sprintf(payloadTemplate, requestor, "null", server.URL+"/")
238		fmt.Fprint(w, payloadBody)
239	})
240
241	return server
242}
243
244func assertDownloadedCorrectFiles(t *testing.T, targetDir string) {
245	expectedFiles := []struct {
246		desc     string
247		path     string
248		contents string
249	}{
250		{
251			desc:     "a file in the exercise root directory",
252			path:     filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-1.txt"),
253			contents: "this is file 1",
254		},
255		{
256			desc:     "a file in a subdirectory",
257			path:     filepath.Join(targetDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"),
258			contents: "this is file 2",
259		},
260	}
261
262	for _, file := range expectedFiles {
263		t.Run(file.desc, func(t *testing.T) {
264			b, err := ioutil.ReadFile(file.path)
265			assert.NoError(t, err)
266			assert.Equal(t, file.contents, string(b))
267		})
268	}
269
270	path := filepath.Join(targetDir, "bogus-track", "bogus-exercise", "file-3.txt")
271	_, err := os.Lstat(path)
272	assert.True(t, os.IsNotExist(err), "It should not write the file if empty.")
273}
274
275func TestDownloadError(t *testing.T) {
276	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
277		w.WriteHeader(http.StatusBadRequest)
278		fmt.Fprintf(w, `{"error": {"type": "error", "message": "test error"}}`)
279	})
280
281	ts := httptest.NewServer(handler)
282	defer ts.Close()
283
284	tmpDir, err := ioutil.TempDir("", "submit-err-tmp-dir")
285	defer os.RemoveAll(tmpDir)
286	assert.NoError(t, err)
287
288	v := viper.New()
289	v.Set("token", "abc123")
290	v.Set("workspace", tmpDir)
291	v.Set("apibaseurl", ts.URL)
292
293	cfg := config.Config{
294		Persister:       config.InMemoryPersister{},
295		UserViperConfig: v,
296		DefaultBaseURL:  "http://example.com",
297	}
298
299	flags := pflag.NewFlagSet("fake", pflag.PanicOnError)
300	setupDownloadFlags(flags)
301	flags.Set("uuid", "value")
302
303	err = runDownload(cfg, flags, []string{})
304
305	assert.Equal(t, "test error", err.Error())
306
307}
308
309const payloadTemplate = `
310{
311	"solution": {
312		"id": "bogus-id",
313		"user": {
314			"handle": "alice",
315			"is_requester": %s
316		},
317		"team": %s,
318		"exercise": {
319			"id": "bogus-exercise",
320			"instructions_url": "http://example.com/bogus-exercise",
321			"auto_approve": false,
322			"track": {
323				"id": "bogus-track",
324				"language": "Bogus Language"
325			}
326		},
327		"file_download_base_url": "%s",
328		"files": [
329			"file-1.txt",
330			"subdir/file-2.txt",
331			"file-3.txt"
332		],
333		"iteration": {
334			"submitted_at": "2017-08-21t10:11:12.130z"
335		}
336	}
337}
338`
339