github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/fs/rc/rcserver/rcserver_test.go (about)

     1  package rcserver
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"regexp"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	_ "github.com/rclone/rclone/backend/local"
    16  	"github.com/rclone/rclone/fs/rc"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  const (
    22  	testBindAddress = "localhost:0"
    23  	testFs          = "testdata/files"
    24  	remoteURL       = "[" + testFs + "]/" // initial URL path to fetch from that remote
    25  )
    26  
    27  // Test the RC server runs and we can do HTTP fetches from it.
    28  // We'll do the majority of the testing with the httptest framework
    29  func TestRcServer(t *testing.T) {
    30  	opt := rc.DefaultOpt
    31  	opt.HTTPOptions.ListenAddr = testBindAddress
    32  	opt.Enabled = true
    33  	opt.Serve = true
    34  	opt.Files = testFs
    35  	mux := http.NewServeMux()
    36  	rcServer := newServer(&opt, mux)
    37  	assert.NoError(t, rcServer.Serve())
    38  	defer func() {
    39  		rcServer.Close()
    40  		rcServer.Wait()
    41  	}()
    42  	testURL := rcServer.Server.URL()
    43  
    44  	// Do the simplest possible test to check the server is alive
    45  	// Do it a few times to wait for the server to start
    46  	var resp *http.Response
    47  	var err error
    48  	for i := 0; i < 10; i++ {
    49  		resp, err = http.Get(testURL + "file.txt")
    50  		if err == nil {
    51  			break
    52  		}
    53  		time.Sleep(10 * time.Millisecond)
    54  	}
    55  
    56  	require.NoError(t, err)
    57  	body, err := ioutil.ReadAll(resp.Body)
    58  	_ = resp.Body.Close()
    59  
    60  	require.NoError(t, err)
    61  	require.NoError(t, resp.Body.Close())
    62  
    63  	assert.Equal(t, http.StatusOK, resp.StatusCode)
    64  	assert.Equal(t, "this is file1.txt\n", string(body))
    65  }
    66  
    67  type testRun struct {
    68  	Name        string
    69  	URL         string
    70  	Status      int
    71  	Method      string
    72  	Range       string
    73  	Body        string
    74  	ContentType string
    75  	Expected    string
    76  	Contains    *regexp.Regexp
    77  	Headers     map[string]string
    78  }
    79  
    80  // Run a suite of tests
    81  func testServer(t *testing.T, tests []testRun, opt *rc.Options) {
    82  	mux := http.NewServeMux()
    83  	rcServer := newServer(opt, mux)
    84  	for _, test := range tests {
    85  		t.Run(test.Name, func(t *testing.T) {
    86  			method := test.Method
    87  			if method == "" {
    88  				method = "GET"
    89  			}
    90  			var inBody io.Reader
    91  			if test.Body != "" {
    92  				buf := bytes.NewBufferString(test.Body)
    93  				inBody = buf
    94  			}
    95  			req, err := http.NewRequest(method, "http://1.2.3.4/"+test.URL, inBody)
    96  			require.NoError(t, err)
    97  			if test.Range != "" {
    98  				req.Header.Add("Range", test.Range)
    99  			}
   100  			if test.ContentType != "" {
   101  				req.Header.Add("Content-Type", test.ContentType)
   102  			}
   103  
   104  			w := httptest.NewRecorder()
   105  			rcServer.handler(w, req)
   106  			resp := w.Result()
   107  
   108  			assert.Equal(t, test.Status, resp.StatusCode)
   109  			body, err := ioutil.ReadAll(resp.Body)
   110  			require.NoError(t, err)
   111  
   112  			if test.Contains == nil {
   113  				assert.Equal(t, test.Expected, string(body))
   114  			} else {
   115  				assert.True(t, test.Contains.Match(body), fmt.Sprintf("body didn't match: %v: %v", test.Contains, string(body)))
   116  			}
   117  
   118  			for k, v := range test.Headers {
   119  				assert.Equal(t, v, resp.Header.Get(k), k)
   120  			}
   121  		})
   122  	}
   123  }
   124  
   125  // return an enabled rc
   126  func newTestOpt() rc.Options {
   127  	opt := rc.DefaultOpt
   128  	opt.Enabled = true
   129  	return opt
   130  }
   131  
   132  func TestFileServing(t *testing.T) {
   133  	tests := []testRun{{
   134  		Name:   "index",
   135  		URL:    "",
   136  		Status: http.StatusOK,
   137  		Expected: `<pre>
   138  <a href="dir/">dir/</a>
   139  <a href="file.txt">file.txt</a>
   140  </pre>
   141  `,
   142  	}, {
   143  		Name:     "notfound",
   144  		URL:      "notfound",
   145  		Status:   http.StatusNotFound,
   146  		Expected: "404 page not found\n",
   147  	}, {
   148  		Name:     "dirnotfound",
   149  		URL:      "dirnotfound/",
   150  		Status:   http.StatusNotFound,
   151  		Expected: "404 page not found\n",
   152  	}, {
   153  		Name:   "dir",
   154  		URL:    "dir/",
   155  		Status: http.StatusOK,
   156  		Expected: `<pre>
   157  <a href="file2.txt">file2.txt</a>
   158  </pre>
   159  `,
   160  	}, {
   161  		Name:     "file",
   162  		URL:      "file.txt",
   163  		Status:   http.StatusOK,
   164  		Expected: "this is file1.txt\n",
   165  		Headers: map[string]string{
   166  			"Content-Length": "18",
   167  		},
   168  	}, {
   169  		Name:     "file2",
   170  		URL:      "dir/file2.txt",
   171  		Status:   http.StatusOK,
   172  		Expected: "this is dir/file2.txt\n",
   173  	}, {
   174  		Name:     "file-head",
   175  		URL:      "file.txt",
   176  		Method:   "HEAD",
   177  		Status:   http.StatusOK,
   178  		Expected: ``,
   179  		Headers: map[string]string{
   180  			"Content-Length": "18",
   181  		},
   182  	}, {
   183  		Name:     "file-range",
   184  		URL:      "file.txt",
   185  		Status:   http.StatusPartialContent,
   186  		Range:    "bytes=8-12",
   187  		Expected: `file1`,
   188  	}}
   189  	opt := newTestOpt()
   190  	opt.Serve = true
   191  	opt.Files = testFs
   192  	testServer(t, tests, &opt)
   193  }
   194  
   195  func TestRemoteServing(t *testing.T) {
   196  	tests := []testRun{
   197  		// Test serving files from the test remote
   198  		{
   199  			Name:   "index",
   200  			URL:    remoteURL + "",
   201  			Status: http.StatusOK,
   202  			Expected: `<!DOCTYPE html>
   203  <html lang="en">
   204  <head>
   205  <meta charset="utf-8">
   206  <title>Directory listing of /</title>
   207  </head>
   208  <body>
   209  <h1>Directory listing of /</h1>
   210  <a href="dir/">dir/</a><br />
   211  <a href="file.txt">file.txt</a><br />
   212  </body>
   213  </html>
   214  `,
   215  		}, {
   216  			Name:   "notfound-index",
   217  			URL:    "[notfound]/",
   218  			Status: http.StatusNotFound,
   219  			Expected: `{
   220  	"error": "failed to list directory: directory not found",
   221  	"input": null,
   222  	"path": "",
   223  	"status": 404
   224  }
   225  `,
   226  		}, {
   227  			Name:   "notfound",
   228  			URL:    remoteURL + "notfound",
   229  			Status: http.StatusNotFound,
   230  			Expected: `{
   231  	"error": "failed to find object: object not found",
   232  	"input": null,
   233  	"path": "notfound",
   234  	"status": 404
   235  }
   236  `,
   237  		}, {
   238  			Name:   "dirnotfound",
   239  			URL:    remoteURL + "dirnotfound/",
   240  			Status: http.StatusNotFound,
   241  			Expected: `{
   242  	"error": "failed to list directory: directory not found",
   243  	"input": null,
   244  	"path": "dirnotfound",
   245  	"status": 404
   246  }
   247  `,
   248  		}, {
   249  			Name:   "dir",
   250  			URL:    remoteURL + "dir/",
   251  			Status: http.StatusOK,
   252  			Expected: `<!DOCTYPE html>
   253  <html lang="en">
   254  <head>
   255  <meta charset="utf-8">
   256  <title>Directory listing of /dir</title>
   257  </head>
   258  <body>
   259  <h1>Directory listing of /dir</h1>
   260  <a href="file2.txt">file2.txt</a><br />
   261  </body>
   262  </html>
   263  `,
   264  		}, {
   265  			Name:     "file",
   266  			URL:      remoteURL + "file.txt",
   267  			Status:   http.StatusOK,
   268  			Expected: "this is file1.txt\n",
   269  			Headers: map[string]string{
   270  				"Content-Length": "18",
   271  			},
   272  		}, {
   273  			Name:     "file with no slash after ]",
   274  			URL:      strings.TrimRight(remoteURL, "/") + "file.txt",
   275  			Status:   http.StatusOK,
   276  			Expected: "this is file1.txt\n",
   277  			Headers: map[string]string{
   278  				"Content-Length": "18",
   279  			},
   280  		}, {
   281  			Name:     "file2",
   282  			URL:      remoteURL + "dir/file2.txt",
   283  			Status:   http.StatusOK,
   284  			Expected: "this is dir/file2.txt\n",
   285  		}, {
   286  			Name:     "file-head",
   287  			URL:      remoteURL + "file.txt",
   288  			Method:   "HEAD",
   289  			Status:   http.StatusOK,
   290  			Expected: ``,
   291  			Headers: map[string]string{
   292  				"Content-Length": "18",
   293  			},
   294  		}, {
   295  			Name:     "file-range",
   296  			URL:      remoteURL + "file.txt",
   297  			Status:   http.StatusPartialContent,
   298  			Range:    "bytes=8-12",
   299  			Expected: `file1`,
   300  		}, {
   301  			Name:   "bad-remote",
   302  			URL:    "[notfoundremote:]/",
   303  			Status: http.StatusInternalServerError,
   304  			Expected: `{
   305  	"error": "failed to make Fs: didn't find section in config file",
   306  	"input": null,
   307  	"path": "/",
   308  	"status": 500
   309  }
   310  `,
   311  		}}
   312  	opt := newTestOpt()
   313  	opt.Serve = true
   314  	opt.Files = testFs
   315  	testServer(t, tests, &opt)
   316  }
   317  
   318  func TestRC(t *testing.T) {
   319  	tests := []testRun{{
   320  		Name:   "rc-root",
   321  		URL:    "",
   322  		Method: "POST",
   323  		Status: http.StatusNotFound,
   324  		Expected: `{
   325  	"error": "couldn't find method \"\"",
   326  	"input": {},
   327  	"path": "",
   328  	"status": 404
   329  }
   330  `,
   331  	}, {
   332  		Name:     "rc-noop",
   333  		URL:      "rc/noop",
   334  		Method:   "POST",
   335  		Status:   http.StatusOK,
   336  		Expected: "{}\n",
   337  	}, {
   338  		Name:   "rc-error",
   339  		URL:    "rc/error",
   340  		Method: "POST",
   341  		Status: http.StatusInternalServerError,
   342  		Expected: `{
   343  	"error": "arbitrary error on input map[]",
   344  	"input": {},
   345  	"path": "rc/error",
   346  	"status": 500
   347  }
   348  `,
   349  	}, {
   350  		Name:     "core-gc",
   351  		URL:      "core/gc", // returns nil, nil so check it is made into {}
   352  		Method:   "POST",
   353  		Status:   http.StatusOK,
   354  		Expected: "{}\n",
   355  	}, {
   356  		Name:   "url-params",
   357  		URL:    "rc/noop?param1=potato&param2=sausage",
   358  		Method: "POST",
   359  		Status: http.StatusOK,
   360  		Expected: `{
   361  	"param1": "potato",
   362  	"param2": "sausage"
   363  }
   364  `,
   365  	}, {
   366  		Name:        "json",
   367  		URL:         "rc/noop",
   368  		Method:      "POST",
   369  		Body:        `{ "param1":"string", "param2":true }`,
   370  		ContentType: "application/json",
   371  		Status:      http.StatusOK,
   372  		Expected: `{
   373  	"param1": "string",
   374  	"param2": true
   375  }
   376  `,
   377  	}, {
   378  		Name:        "json-and-url-params",
   379  		URL:         "rc/noop?param1=potato&param2=sausage",
   380  		Method:      "POST",
   381  		Body:        `{ "param1":"string", "param3":true }`,
   382  		ContentType: "application/json",
   383  		Status:      http.StatusOK,
   384  		Expected: `{
   385  	"param1": "string",
   386  	"param2": "sausage",
   387  	"param3": true
   388  }
   389  `,
   390  	}, {
   391  		Name:        "json-bad",
   392  		URL:         "rc/noop?param1=potato&param2=sausage",
   393  		Method:      "POST",
   394  		Body:        `{ param1":"string", "param3":true }`,
   395  		ContentType: "application/json",
   396  		Status:      http.StatusBadRequest,
   397  		Expected: `{
   398  	"error": "failed to read input JSON: invalid character 'p' looking for beginning of object key string",
   399  	"input": {
   400  		"param1": "potato",
   401  		"param2": "sausage"
   402  	},
   403  	"path": "rc/noop",
   404  	"status": 400
   405  }
   406  `,
   407  	}, {
   408  		Name:        "form",
   409  		URL:         "rc/noop",
   410  		Method:      "POST",
   411  		Body:        `param1=string&param2=true`,
   412  		ContentType: "application/x-www-form-urlencoded",
   413  		Status:      http.StatusOK,
   414  		Expected: `{
   415  	"param1": "string",
   416  	"param2": "true"
   417  }
   418  `,
   419  	}, {
   420  		Name:        "form-and-url-params",
   421  		URL:         "rc/noop?param1=potato&param2=sausage",
   422  		Method:      "POST",
   423  		Body:        `param1=string&param3=true`,
   424  		ContentType: "application/x-www-form-urlencoded",
   425  		Status:      http.StatusOK,
   426  		Expected: `{
   427  	"param1": "potato",
   428  	"param2": "sausage",
   429  	"param3": "true"
   430  }
   431  `,
   432  	}, {
   433  		Name:        "form-bad",
   434  		URL:         "rc/noop?param1=potato&param2=sausage",
   435  		Method:      "POST",
   436  		Body:        `%zz`,
   437  		ContentType: "application/x-www-form-urlencoded",
   438  		Status:      http.StatusBadRequest,
   439  		Expected: `{
   440  	"error": "failed to parse form/URL parameters: invalid URL escape \"%zz\"",
   441  	"input": null,
   442  	"path": "rc/noop",
   443  	"status": 400
   444  }
   445  `,
   446  	}}
   447  	opt := newTestOpt()
   448  	opt.Serve = true
   449  	opt.Files = testFs
   450  	testServer(t, tests, &opt)
   451  }
   452  
   453  func TestMethods(t *testing.T) {
   454  	tests := []testRun{{
   455  		Name:     "options",
   456  		URL:      "",
   457  		Method:   "OPTIONS",
   458  		Status:   http.StatusOK,
   459  		Expected: "",
   460  		Headers: map[string]string{
   461  			"Access-Control-Allow-Origin":   "http://localhost:5572/",
   462  			"Access-Control-Request-Method": "POST, OPTIONS, GET, HEAD",
   463  			"Access-Control-Allow-Headers":  "authorization, Content-Type",
   464  		},
   465  	}, {
   466  		Name:   "bad",
   467  		URL:    "",
   468  		Method: "POTATO",
   469  		Status: http.StatusMethodNotAllowed,
   470  		Expected: `{
   471  	"error": "method \"POTATO\" not allowed",
   472  	"input": null,
   473  	"path": "",
   474  	"status": 405
   475  }
   476  `,
   477  	}}
   478  	opt := newTestOpt()
   479  	opt.Serve = true
   480  	opt.Files = testFs
   481  	testServer(t, tests, &opt)
   482  }
   483  
   484  var matchRemoteDirListing = regexp.MustCompile(`<title>List of all rclone remotes.</title>`)
   485  
   486  func TestServingRoot(t *testing.T) {
   487  	tests := []testRun{{
   488  		Name:     "rootlist",
   489  		URL:      "*",
   490  		Status:   http.StatusOK,
   491  		Contains: matchRemoteDirListing,
   492  	}}
   493  	opt := newTestOpt()
   494  	opt.Serve = true
   495  	opt.Files = testFs
   496  	testServer(t, tests, &opt)
   497  }
   498  
   499  func TestServingRootNoFiles(t *testing.T) {
   500  	tests := []testRun{{
   501  		Name:     "rootlist",
   502  		URL:      "",
   503  		Status:   http.StatusOK,
   504  		Contains: matchRemoteDirListing,
   505  	}}
   506  	opt := newTestOpt()
   507  	opt.Serve = true
   508  	opt.Files = ""
   509  	testServer(t, tests, &opt)
   510  }
   511  
   512  func TestNoFiles(t *testing.T) {
   513  	tests := []testRun{{
   514  		Name:     "file",
   515  		URL:      "file.txt",
   516  		Status:   http.StatusNotFound,
   517  		Expected: "Not Found\n",
   518  	}, {
   519  		Name:     "dir",
   520  		URL:      "dir/",
   521  		Status:   http.StatusNotFound,
   522  		Expected: "Not Found\n",
   523  	}}
   524  	opt := newTestOpt()
   525  	opt.Serve = true
   526  	opt.Files = ""
   527  	testServer(t, tests, &opt)
   528  }
   529  
   530  func TestNoServe(t *testing.T) {
   531  	tests := []testRun{{
   532  		Name:     "file",
   533  		URL:      remoteURL + "file.txt",
   534  		Status:   http.StatusNotFound,
   535  		Expected: "404 page not found\n",
   536  	}, {
   537  		Name:     "dir",
   538  		URL:      remoteURL + "dir/",
   539  		Status:   http.StatusNotFound,
   540  		Expected: "404 page not found\n",
   541  	}}
   542  	opt := newTestOpt()
   543  	opt.Serve = false
   544  	opt.Files = testFs
   545  	testServer(t, tests, &opt)
   546  }
   547  
   548  func TestAuthRequired(t *testing.T) {
   549  	tests := []testRun{{
   550  		Name:        "auth",
   551  		URL:         "rc/noopauth",
   552  		Method:      "POST",
   553  		Body:        `{}`,
   554  		ContentType: "application/javascript",
   555  		Status:      http.StatusForbidden,
   556  		Expected: `{
   557  	"error": "authentication must be set up on the rc server to use \"rc/noopauth\" or the --rc-no-auth flag must be in use",
   558  	"input": {},
   559  	"path": "rc/noopauth",
   560  	"status": 403
   561  }
   562  `,
   563  	}}
   564  	opt := newTestOpt()
   565  	opt.Serve = false
   566  	opt.Files = ""
   567  	opt.NoAuth = false
   568  	testServer(t, tests, &opt)
   569  }
   570  
   571  func TestNoAuth(t *testing.T) {
   572  	tests := []testRun{{
   573  		Name:        "auth",
   574  		URL:         "rc/noopauth",
   575  		Method:      "POST",
   576  		Body:        `{}`,
   577  		ContentType: "application/javascript",
   578  		Status:      http.StatusOK,
   579  		Expected:    "{}\n",
   580  	}}
   581  	opt := newTestOpt()
   582  	opt.Serve = false
   583  	opt.Files = ""
   584  	opt.NoAuth = true
   585  	testServer(t, tests, &opt)
   586  }
   587  
   588  func TestWithUserPass(t *testing.T) {
   589  	tests := []testRun{{
   590  		Name:        "auth",
   591  		URL:         "rc/noopauth",
   592  		Method:      "POST",
   593  		Body:        `{}`,
   594  		ContentType: "application/javascript",
   595  		Status:      http.StatusOK,
   596  		Expected:    "{}\n",
   597  	}}
   598  	opt := newTestOpt()
   599  	opt.Serve = false
   600  	opt.Files = ""
   601  	opt.NoAuth = false
   602  	opt.HTTPOptions.BasicUser = "user"
   603  	opt.HTTPOptions.BasicPass = "pass"
   604  	testServer(t, tests, &opt)
   605  }
   606  
   607  func TestRCAsync(t *testing.T) {
   608  	tests := []testRun{{
   609  		Name:        "ok",
   610  		URL:         "rc/noop",
   611  		Method:      "POST",
   612  		ContentType: "application/json",
   613  		Body:        `{ "_async":true }`,
   614  		Status:      http.StatusOK,
   615  		Contains:    regexp.MustCompile(`(?s)\{.*\"jobid\":.*\}`),
   616  	}, {
   617  		Name:        "bad",
   618  		URL:         "rc/noop",
   619  		Method:      "POST",
   620  		ContentType: "application/json",
   621  		Body:        `{ "_async":"truthy" }`,
   622  		Status:      http.StatusBadRequest,
   623  		Expected: `{
   624  	"error": "couldn't parse key \"_async\" (truthy) as bool: strconv.ParseBool: parsing \"truthy\": invalid syntax",
   625  	"input": {
   626  		"_async": "truthy"
   627  	},
   628  	"path": "rc/noop",
   629  	"status": 400
   630  }
   631  `,
   632  	}}
   633  	opt := newTestOpt()
   634  	opt.Serve = true
   635  	opt.Files = ""
   636  	testServer(t, tests, &opt)
   637  }