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¶m2=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¶m2=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¶m2=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¶m2=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¶m2=sausage", 427 Method: "POST", 428 Body: `param1=string¶m3=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¶m2=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 }