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