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