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