github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/api/handlers_test.go (about) 1 package api 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "mime/multipart" 11 "net/http" 12 "net/http/httptest" 13 "net/url" 14 "os" 15 "path/filepath" 16 "runtime" 17 "strconv" 18 "strings" 19 "testing" 20 21 "github.com/google/go-cmp/cmp" 22 "github.com/gorilla/mux" 23 "github.com/qri-io/dataset" 24 "github.com/qri-io/dataset/dstest" 25 "github.com/qri-io/qri/lib" 26 ) 27 28 func TestGetZip(t *testing.T) { 29 run := NewAPITestRunner(t) 30 defer run.Delete() 31 32 // Save a version of the dataset 33 ds := run.BuildDataset("test_ds") 34 ds.Meta = &dataset.Meta{Title: "some title"} 35 ds.Readme = &dataset.Readme{Text: "# hi\n\nthis is a readme"} 36 run.SaveDataset(ds, "testdata/cities/data.csv") 37 38 // Get a zip file binary over the API 39 gotStatusCode, gotBodyString := APICall("/get/peer/test_ds?format=zip", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds"}) 40 if gotStatusCode != 200 { 41 t.Fatalf("expected status code 200, got %d", gotStatusCode) 42 } 43 44 // Compare the API response to the expected zip file 45 expectBytes, err := ioutil.ReadFile("testdata/cities/exported.zip") 46 if err != nil { 47 t.Fatalf("error reading expected bytes: %s", err) 48 } 49 if diff := cmp.Diff(string(expectBytes), gotBodyString); diff != "" { 50 t.Errorf("byte mismatch (-want +got):\n%s", diff) 51 } 52 } 53 54 func TestGetBodyCSVHandler(t *testing.T) { 55 run := NewAPITestRunner(t) 56 defer run.Delete() 57 58 ds := dataset.Dataset{ 59 Name: "test_ds", 60 Meta: &dataset.Meta{ 61 Title: "title one", 62 }, 63 } 64 run.SaveDataset(&ds, "testdata/cities/data.csv") 65 66 // Get csv body using "body.csv" suffix 67 actualStatusCode, actualBody := APICall("/get/peer/test_ds/body.csv", GetBodyCSVHandler(run.Inst), map[string]string{"username": "peer", "name": "test_ds"}) 68 expectBody := "city,pop,avg_age,in_usa\ntoronto,40000000,55.5,false\nnew york,8500000,44.4,true\nchicago,300000,44.4,true\nchatham,35000,65.25,true\nraleigh,250000,50.65,true\n" 69 assertStatusCode(t, "get body.csv using suffix", actualStatusCode, 200) 70 if diff := cmp.Diff(expectBody, actualBody); diff != "" { 71 t.Errorf("output mismatch (-want +got):\n%s", diff) 72 } 73 74 // incorrect http method 75 actualStatusCode, actualBody = APICallWithParams("POST", "/get/peer/test_ds/body.csv", nil, GetBodyCSVHandler(run.Inst), nil) 76 assertStatusCode(t, "get body.csv with incorrect http method", actualStatusCode, 404) 77 78 // invalid request 79 actualStatusCode, actualBody = APICall("/get/peer/test_ds/body.csv", GetBodyCSVHandler(run.Inst), map[string]string{"username": "peer", "name": "test_ds", "format": "json"}) 80 assertStatusCode(t, "get body.csv with incorrect http method", actualStatusCode, 400) 81 } 82 83 func TestDatasetGet(t *testing.T) { 84 run := NewAPITestRunner(t) 85 defer run.Delete() 86 87 ds := dataset.Dataset{ 88 Name: "test_ds", 89 Meta: &dataset.Meta{ 90 Title: "title one", 91 }, 92 Readme: &dataset.Readme{ 93 Text: "hello world", 94 }, 95 } 96 run.SaveDataset(&ds, "testdata/cities/data.csv") 97 98 actualStatusCode, actualBody := APICall("/get/peer/test_ds", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds"}) 99 assertStatusCode(t, "get dataset", actualStatusCode, 200) 100 got := datasetJSONResponse(t, actualBody) 101 dstest.CompareGoldenDatasetAndUpdateIfEnvVarSet(t, "testdata/expect/TestDatasetGet.test_ds.json", got) 102 103 // Can get csv body file using format 104 actualStatusCode, _ = APICall("/get/peer/test_ds/body?format=csv", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds", "selector": "body"}) 105 assertStatusCode(t, "get csv file using format", actualStatusCode, 200) 106 107 // Can get zip file 108 actualStatusCode, _ = APICall("/get/peer/test_ds?format=zip", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds"}) 109 assertStatusCode(t, "get zip file", actualStatusCode, 200) 110 111 // Can get a readme script 112 actualStatusCode, _ = APICall("/get/peer/test_ds/readme.script", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds", "selector": "readme.script"}) 113 assertStatusCode(t, "get readme.script", actualStatusCode, 200) 114 115 // Can get a single component 116 actualStatusCode, _ = APICall("/get/peer/test_ds/meta", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds", "selector": "meta"}) 117 assertStatusCode(t, "get meta component", actualStatusCode, 200) 118 119 // Can get at an ipfs version 120 actualStatusCode, _ = APICall("/get/peer/test_ds/at/mem/QmX3Y2CG4DhZMHKTPAGPpLdwRPoWDjZLxAJwcikNYo8Tqa", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds", "fs": "mem", "hash": "QmX3Y2CG4DhZMHKTPAGPpLdwRPoWDjZLxAJwcikNYo8Tqa"}) 121 assertStatusCode(t, "get at content-addressed version", actualStatusCode, 200) 122 123 // Error 404 if ipfs version doesn't exist 124 actualStatusCode, _ = APICall("/get/peer/test_ds/at/mem/QmissingEJUqFWNfdiPTPtxyba6wf86TmbQe1nifpZCRH6", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds", "fs": "mem", "hash": "QmissingEJUqFWNfdiPTPtxyba6wf86TmbQe1nifpZCRH6"}) 125 assertStatusCode(t, "get missing content-addressed version", actualStatusCode, 404) 126 127 // Error 400 due to unknown component 128 actualStatusCode, _ = APICall("/get/peer/test_ds/dunno", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test_ds", "selector": "dunno"}) 129 assertStatusCode(t, "unknown component", actualStatusCode, 400) 130 131 // Error 400 due to parse error of dsref 132 actualStatusCode, _ = APICall("/get/peer/test+ds", GetHandler(run.Inst, ""), map[string]string{"username": "peer", "name": "test+ds"}) 133 assertStatusCode(t, "invalid dsref", actualStatusCode, 400) 134 } 135 136 func TestUnpackHandler(t *testing.T) { 137 buf := new(bytes.Buffer) 138 zw := zip.NewWriter(buf) 139 140 text := []byte(`{ "meta": { "title": "hello world!" }}`) 141 filename := "meta.json" 142 f, err := zw.Create(filename) 143 if err != nil { 144 t.Fatal(err) 145 } 146 _, err = f.Write(text) 147 if err != nil { 148 t.Fatal(err) 149 } 150 zw.Close() 151 152 rr := bytes.NewReader(buf.Bytes()) 153 154 r := httptest.NewRequest("POST", "/unpack", rr) 155 w := httptest.NewRecorder() 156 157 hf := UnpackHandler("/unpack") 158 hf(w, r) 159 if w.Result().StatusCode != 200 { 160 t.Errorf("%s", w.Body.String()) 161 t.Fatal(fmt.Errorf("expected unpack handler to return with 200 status code, returned with %d", w.Result().StatusCode)) 162 } 163 164 r = httptest.NewRequest("GET", "/unpack", nil) 165 w = httptest.NewRecorder() 166 hf(w, r) 167 if w.Result().StatusCode != 404 { 168 t.Errorf("%s", w.Body.String()) 169 t.Fatal(fmt.Errorf("expected call to unpack handler with GET method to return status code 404, got %d", w.Result().StatusCode)) 170 } 171 172 r = httptest.NewRequest("POST", "/unpack", nil) 173 w = httptest.NewRecorder() 174 hf(w, r) 175 if w.Result().StatusCode != 500 { 176 t.Errorf("%s", w.Body.String()) 177 t.Fatal(fmt.Errorf("expected call to unpack handler with GET method to return status code 500, got %d", w.Result().StatusCode)) 178 } 179 } 180 181 func TestSaveByUploadHandler(t *testing.T) { 182 run := NewAPITestRunner(t) 183 defer run.Delete() 184 185 r := newFormFileRequest(t, "/ds/save/upload", map[string]string{ 186 "file": dstestTestdataFile("cities/init_dataset.json"), 187 "viz": dstestTestdataFile("cities/template.html"), 188 "transform": dstestTestdataFile("cities/transform.star"), 189 "readme": dstestTestdataFile("cities/readme.md"), 190 "body": dstestTestdataFile("cities/data.csv"), 191 }, map[string]string{ 192 "ref": "peer/test_form_upload", 193 "apply": "false", 194 }) 195 196 w := httptest.NewRecorder() 197 h := SaveByUploadHandler(run.Instance(), "/ds/save/upload") 198 h(w, r) 199 res := w.Result() 200 statusCode := res.StatusCode 201 bodyBytes, err := ioutil.ReadAll(res.Body) 202 if err != nil { 203 panic(err) 204 } 205 assertStatusCode(t, "SaveByUploadHandler unexpected status code", statusCode, 200) 206 got := datasetJSONResponse(t, string(bodyBytes)) 207 dstest.CompareGoldenDatasetAndUpdateIfEnvVarSet(t, "testdata/expect/TestSaveByUpload.test_ds.json", got) 208 } 209 210 func assertStatusCode(t *testing.T, description string, actualStatusCode, expectStatusCode int) { 211 t.Helper() 212 if expectStatusCode != actualStatusCode { 213 t.Errorf("%s: expected status code %d, got %d", description, expectStatusCode, actualStatusCode) 214 } 215 } 216 217 func datasetJSONResponse(t *testing.T, body string) *dataset.Dataset { 218 t.Helper() 219 res := struct { 220 Data *dataset.Dataset 221 Meta map[string]interface{} 222 }{} 223 if err := json.Unmarshal([]byte(body), &res); err != nil { 224 t.Fatal(err) 225 } 226 return res.Data 227 } 228 229 func TestValidateCSVRequest(t *testing.T) { 230 var caseName string 231 var expectErr error 232 var r *http.Request 233 var p *lib.GetParams 234 235 // bad selector 236 caseName = "selector is not body" 237 r, _ = http.NewRequest("GET", "", nil) 238 p = &lib.GetParams{} 239 expectErr = fmt.Errorf("can only get csv of the body component, selector must be 'body'") 240 err := validateCSVRequest(r, p) 241 if expectErr.Error() != err.Error() { 242 t.Errorf("case %q, expected error %q, got %q", caseName, expectErr, err) 243 } 244 245 // add body selector to params 246 p.Selector = "body" 247 248 // bad format 249 caseName = "bad format" 250 r, _ = http.NewRequest("GET", "", nil) 251 r = mustSetMuxVarsOnRequest(t, r, map[string]string{"username": "me", "name": "my_ds", "format": "json"}) 252 expectErr = fmt.Errorf("format \"json\" conflicts with requested body csv file") 253 err = validateCSVRequest(r, p) 254 if expectErr.Error() != err.Error() { 255 t.Errorf("case %q, expected error %q, got %q", caseName, expectErr, err) 256 } 257 258 // valid request 259 caseName = "valid request" 260 r, _ = http.NewRequest("GET", "", nil) 261 r = mustSetMuxVarsOnRequest(t, r, map[string]string{"username": "peer", "name": "my_ds", "format": "csv"}) 262 err = validateCSVRequest(r, p) 263 if err != nil { 264 t.Errorf("case %q, unexpected error %q", caseName, err) 265 } 266 } 267 268 func TestValidateZipRequest(t *testing.T) { 269 var caseName string 270 var expectErr error 271 var r *http.Request 272 var p *lib.GetParams 273 274 // bad selector 275 caseName = "selector is not empty" 276 r, _ = http.NewRequest("GET", "", nil) 277 p = &lib.GetParams{Selector: "meta"} 278 expectErr = fmt.Errorf("can only get zip file of the entire dataset, got selector \"meta\"") 279 err := validateZipRequest(r, p) 280 if expectErr.Error() != err.Error() { 281 t.Errorf("case %q, expected error %q, got %q", caseName, expectErr, err) 282 } 283 284 // remove selector from params 285 p.Selector = "" 286 287 // bad format 288 caseName = "bad format" 289 r, _ = http.NewRequest("GET", "", nil) 290 r = mustSetMuxVarsOnRequest(t, r, map[string]string{"username": "me", "name": "my_ds", "format": "json"}) 291 expectErr = fmt.Errorf("format %q conflicts with header %q", "json", "Accept: application/zip") 292 err = validateZipRequest(r, p) 293 if expectErr.Error() != err.Error() { 294 t.Errorf("case %q, expected error %q, got %q", caseName, expectErr, err) 295 } 296 297 // valid request 298 caseName = "valid request" 299 r, _ = http.NewRequest("GET", "", nil) 300 r = mustSetMuxVarsOnRequest(t, r, map[string]string{"username": "peer", "name": "my_ds", "format": "zip"}) 301 err = validateZipRequest(r, p) 302 if err != nil { 303 t.Errorf("case %q, unexpected error %q", caseName, err) 304 } 305 } 306 307 func mustSetMuxVarsOnRequest(t *testing.T, r *http.Request, muxVars map[string]string) *http.Request { 308 r = mux.SetURLVars(r, muxVars) 309 setRefStringFromMuxVars(r) 310 if err := setMuxVarsToQueryParams(r); err != nil { 311 t.Fatal(err) 312 } 313 return r 314 } 315 316 func TestExtensionToMimeType(t *testing.T) { 317 cases := []struct { 318 ext, expect string 319 }{ 320 {".csv", "text/csv"}, 321 {".json", "application/json"}, 322 {".yaml", "application/x-yaml"}, 323 {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, 324 {".zip", "application/zip"}, 325 {".txt", "text/plain"}, 326 {".md", "text/x-markdown"}, 327 {".html", "text/html"}, 328 {"", ""}, 329 } 330 for i, c := range cases { 331 got := extensionToMimeType(c.ext) 332 if c.expect != got { 333 t.Errorf("case %d: expected %q got %q", i, c.expect, got) 334 } 335 } 336 } 337 338 func newFormFileRequest(t *testing.T, url string, files, params map[string]string) *http.Request { 339 body := &bytes.Buffer{} 340 writer := multipart.NewWriter(body) 341 342 for name, path := range files { 343 data, err := os.Open(path) 344 if err != nil { 345 t.Fatalf("error opening datafile: %s %s", name, err) 346 } 347 dataPart, err := writer.CreateFormFile(name, filepath.Base(path)) 348 if err != nil { 349 t.Fatalf("error adding data file to form: %s %s", name, err) 350 } 351 352 if _, err := io.Copy(dataPart, data); err != nil { 353 t.Fatalf("error copying data: %s", err) 354 } 355 } 356 357 for key, val := range params { 358 if err := writer.WriteField(key, val); err != nil { 359 t.Fatalf("error adding field to writer: %s", err) 360 } 361 } 362 363 if err := writer.Close(); err != nil { 364 t.Fatalf("error closing writer: %s", err) 365 } 366 367 req := httptest.NewRequest("POST", url, body) 368 req.Header.Add("Content-Type", writer.FormDataContentType()) 369 return req 370 } 371 372 func dstestTestdataFile(path string) string { 373 _, currfile, _, _ := runtime.Caller(0) 374 testdataPath := filepath.Join(filepath.Dir(currfile), "testdata") 375 return filepath.Join(testdataPath, path) 376 } 377 378 // APICall calls the api and returns the status code and body 379 func APICall(url string, hf http.HandlerFunc, muxVars map[string]string) (int, string) { 380 return APICallWithParams("GET", url, nil, hf, muxVars) 381 } 382 383 // APICallWithParams calls the api and returns the status code and body 384 func APICallWithParams(method, reqURL string, params map[string]string, hf http.HandlerFunc, muxVars map[string]string) (int, string) { 385 // Add parameters from map 386 reqParams := url.Values{} 387 if params != nil { 388 for key := range params { 389 reqParams.Set(key, params[key]) 390 } 391 } 392 req := httptest.NewRequest(method, reqURL, strings.NewReader(reqParams.Encode())) 393 if muxVars != nil { 394 req = mux.SetURLVars(req, muxVars) 395 } 396 setRefStringFromMuxVars(req) 397 if err := setMuxVarsToQueryParams(req); err != nil { 398 panic(err) 399 } 400 // Set form-encoded header so server will find the parameters 401 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 402 req.Header.Add("Content-Length", strconv.Itoa(len(reqParams.Encode()))) 403 w := httptest.NewRecorder() 404 hf(w, req) 405 res := w.Result() 406 statusCode := res.StatusCode 407 bodyBytes, err := ioutil.ReadAll(res.Body) 408 if err != nil { 409 panic(err) 410 } 411 return statusCode, string(bodyBytes) 412 } 413 414 func JSONAPICallWithBody(method, reqURL string, data interface{}, hf http.HandlerFunc, muxVars map[string]string) (int, string) { 415 enc, err := json.Marshal(data) 416 if err != nil { 417 panic(err) 418 } 419 420 req := httptest.NewRequest(method, reqURL, bytes.NewReader(enc)) 421 if muxVars != nil { 422 req = mux.SetURLVars(req, muxVars) 423 } 424 setRefStringFromMuxVars(req) 425 // Set form-encoded header so server will find the parameters 426 req.Header.Add("Content-Type", "application/json") 427 w := httptest.NewRecorder() 428 hf(w, req) 429 res := w.Result() 430 statusCode := res.StatusCode 431 432 bodyBytes, err := ioutil.ReadAll(res.Body) 433 if err != nil { 434 panic(err) 435 } 436 return statusCode, string(bodyBytes) 437 }