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  }