github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/rc/rcserver/rcserver_test.go (about) 1 package rcserver 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 "testing" 16 "time" 17 18 _ "github.com/rclone/rclone/backend/local" 19 "github.com/rclone/rclone/fs" 20 "github.com/rclone/rclone/fs/accounting" 21 "github.com/rclone/rclone/fs/config/configfile" 22 "github.com/rclone/rclone/fs/rc" 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/require" 25 ) 26 27 const ( 28 testBindAddress = "localhost:0" 29 defaultTestTemplate = "testdata/golden/testindex.html" 30 testFs = "testdata/files" 31 remoteURL = "[" + testFs + "]/" // initial URL path to fetch from that remote 32 ) 33 34 func TestMain(m *testing.M) { 35 // Pretend to be rclone version if we have a version string parameter 36 if os.Args[len(os.Args)-1] == "version" { 37 fmt.Printf("rclone %s\n", fs.Version) 38 os.Exit(0) 39 } 40 // Pretend to error if we have an unknown command 41 if os.Args[len(os.Args)-1] == "unknown_command" { 42 fmt.Printf("rclone %s\n", fs.Version) 43 fmt.Fprintf(os.Stderr, "Unknown command\n") 44 os.Exit(1) 45 } 46 os.Exit(m.Run()) 47 } 48 49 // Test the RC server runs and we can do HTTP fetches from it. 50 // We'll do the majority of the testing with the httptest framework 51 func TestRcServer(t *testing.T) { 52 opt := rc.DefaultOpt 53 opt.HTTP.ListenAddr = []string{testBindAddress} 54 opt.Template.Path = defaultTestTemplate 55 opt.Enabled = true 56 opt.Serve = true 57 opt.Files = testFs 58 mux := http.NewServeMux() 59 rcServer, err := newServer(context.Background(), &opt, mux) 60 require.NoError(t, err) 61 assert.NoError(t, rcServer.Serve()) 62 defer func() { 63 assert.NoError(t, rcServer.Shutdown()) 64 rcServer.Wait() 65 }() 66 testURL := rcServer.server.URLs()[0] 67 68 // Do the simplest possible test to check the server is alive 69 // Do it a few times to wait for the server to start 70 var resp *http.Response 71 for i := 0; i < 10; i++ { 72 resp, err = http.Get(testURL + "file.txt") 73 if err == nil { 74 break 75 } 76 time.Sleep(10 * time.Millisecond) 77 } 78 79 require.NoError(t, err) 80 body, err := io.ReadAll(resp.Body) 81 _ = resp.Body.Close() 82 83 require.NoError(t, err) 84 require.NoError(t, resp.Body.Close()) 85 86 assert.Equal(t, http.StatusOK, resp.StatusCode) 87 assert.Equal(t, "this is file1.txt\n", string(body)) 88 } 89 90 type testRun struct { 91 Name string 92 URL string 93 User string 94 Pass string 95 Status int 96 Method string 97 Range string 98 Body string 99 ContentType string 100 Expected string 101 Contains *regexp.Regexp 102 Headers map[string]string 103 } 104 105 // Run a suite of tests 106 func testServer(t *testing.T, tests []testRun, opt *rc.Options) { 107 t.Helper() 108 109 ctx := context.Background() 110 configfile.Install() 111 if opt.Template.Path == "" { 112 opt.Template.Path = defaultTestTemplate 113 } 114 rcServer, err := newServer(ctx, opt, http.DefaultServeMux) 115 require.NoError(t, err) 116 testURL := rcServer.server.URLs()[0] 117 mux := rcServer.server.Router() 118 for _, test := range tests { 119 t.Run(test.Name, func(t *testing.T) { 120 t.Helper() 121 122 method := test.Method 123 if method == "" { 124 method = "GET" 125 } 126 var inBody io.Reader 127 if test.Body != "" { 128 buf := bytes.NewBufferString(test.Body) 129 inBody = buf 130 } 131 req, err := http.NewRequest(method, "http://1.2.3.4/"+test.URL, inBody) 132 require.NoError(t, err) 133 if test.Range != "" { 134 req.Header.Add("Range", test.Range) 135 } 136 if test.ContentType != "" { 137 req.Header.Add("Content-Type", test.ContentType) 138 } 139 if test.User != "" && test.Pass != "" { 140 req.SetBasicAuth(test.User, test.Pass) 141 } 142 143 w := httptest.NewRecorder() 144 mux.ServeHTTP(w, req) 145 resp := w.Result() 146 147 assert.Equal(t, test.Status, resp.StatusCode) 148 body, err := io.ReadAll(resp.Body) 149 require.NoError(t, err) 150 151 if test.ContentType == "application/json" && test.Expected != "" { 152 expectedNormalized := normalizeJSON(t, test.Expected) 153 actualNormalized := normalizeJSON(t, string(body)) 154 assert.Equal(t, expectedNormalized, actualNormalized, "Normalized JSON does not match") 155 } else if test.Contains == nil { 156 assert.Equal(t, test.Expected, string(body)) 157 } else { 158 assert.True(t, test.Contains.Match(body), fmt.Sprintf("body didn't match: %v: %v", test.Contains, string(body))) 159 } 160 161 for k, v := range test.Headers { 162 if v == "testURL" { 163 v = testURL 164 } 165 assert.Equal(t, v, resp.Header.Get(k), k) 166 } 167 }) 168 } 169 } 170 171 // return an enabled rc 172 func newTestOpt() rc.Options { 173 opt := rc.DefaultOpt 174 opt.Enabled = true 175 opt.HTTP.ListenAddr = []string{testBindAddress} 176 return opt 177 } 178 179 func TestFileServing(t *testing.T) { 180 tests := []testRun{{ 181 Name: "index", 182 URL: "", 183 Status: http.StatusOK, 184 Expected: `<pre> 185 <a href="dir/">dir/</a> 186 <a href="file.txt">file.txt</a> 187 <a href="modtime/">modtime/</a> 188 </pre> 189 `, 190 }, { 191 Name: "notfound", 192 URL: "notfound", 193 Status: http.StatusNotFound, 194 Expected: "404 page not found\n", 195 }, { 196 Name: "dirnotfound", 197 URL: "dirnotfound/", 198 Status: http.StatusNotFound, 199 Expected: "404 page not found\n", 200 }, { 201 Name: "dir", 202 URL: "dir/", 203 Status: http.StatusOK, 204 Expected: `<pre> 205 <a href="file2.txt">file2.txt</a> 206 </pre> 207 `, 208 }, { 209 Name: "file", 210 URL: "file.txt", 211 Status: http.StatusOK, 212 Expected: "this is file1.txt\n", 213 Headers: map[string]string{ 214 "Content-Length": "18", 215 }, 216 }, { 217 Name: "file2", 218 URL: "dir/file2.txt", 219 Status: http.StatusOK, 220 Expected: "this is dir/file2.txt\n", 221 }, { 222 Name: "file-head", 223 URL: "file.txt", 224 Method: "HEAD", 225 Status: http.StatusOK, 226 Expected: ``, 227 Headers: map[string]string{ 228 "Content-Length": "18", 229 }, 230 }, { 231 Name: "file-range", 232 URL: "file.txt", 233 Status: http.StatusPartialContent, 234 Range: "bytes=8-12", 235 Expected: `file1`, 236 }} 237 opt := newTestOpt() 238 opt.Serve = true 239 opt.Files = testFs 240 testServer(t, tests, &opt) 241 } 242 243 func TestRemoteServing(t *testing.T) { 244 tests := []testRun{ 245 // Test serving files from the test remote 246 { 247 Name: "index", 248 URL: remoteURL + "", 249 Status: http.StatusOK, 250 Expected: `<!DOCTYPE html> 251 <html lang="en"> 252 <head> 253 <meta charset="utf-8"> 254 <title>Directory listing of /</title> 255 </head> 256 <body> 257 <h1>Directory listing of /</h1> 258 <a href="dir/">dir/</a><br /> 259 <a href="modtime/">modtime/</a><br /> 260 <a href="file.txt">file.txt</a><br /> 261 </body> 262 </html> 263 `, 264 }, { 265 Name: "notfound-index", 266 URL: "[notfound]/", 267 Status: http.StatusNotFound, 268 Expected: `{ 269 "error": "failed to list directory: directory not found", 270 "input": null, 271 "path": "", 272 "status": 404 273 } 274 `, 275 }, { 276 Name: "notfound", 277 URL: remoteURL + "notfound", 278 Status: http.StatusNotFound, 279 Expected: `{ 280 "error": "failed to find object: object not found", 281 "input": null, 282 "path": "notfound", 283 "status": 404 284 } 285 `, 286 }, { 287 Name: "dirnotfound", 288 URL: remoteURL + "dirnotfound/", 289 Status: http.StatusNotFound, 290 Expected: `{ 291 "error": "failed to list directory: directory not found", 292 "input": null, 293 "path": "dirnotfound", 294 "status": 404 295 } 296 `, 297 }, { 298 Name: "dir", 299 URL: remoteURL + "dir/", 300 Status: http.StatusOK, 301 Expected: `<!DOCTYPE html> 302 <html lang="en"> 303 <head> 304 <meta charset="utf-8"> 305 <title>Directory listing of /dir</title> 306 </head> 307 <body> 308 <h1>Directory listing of /dir</h1> 309 <a href="file2.txt">file2.txt</a><br /> 310 </body> 311 </html> 312 `, 313 }, { 314 Name: "file", 315 URL: remoteURL + "file.txt", 316 Status: http.StatusOK, 317 Expected: "this is file1.txt\n", 318 Headers: map[string]string{ 319 "Content-Length": "18", 320 }, 321 }, { 322 Name: "file with no slash after ]", 323 URL: strings.TrimRight(remoteURL, "/") + "file.txt", 324 Status: http.StatusOK, 325 Expected: "this is file1.txt\n", 326 Headers: map[string]string{ 327 "Content-Length": "18", 328 }, 329 }, { 330 Name: "file2", 331 URL: remoteURL + "dir/file2.txt", 332 Status: http.StatusOK, 333 Expected: "this is dir/file2.txt\n", 334 }, { 335 Name: "file-head", 336 URL: remoteURL + "file.txt", 337 Method: "HEAD", 338 Status: http.StatusOK, 339 Expected: ``, 340 Headers: map[string]string{ 341 "Content-Length": "18", 342 }, 343 }, { 344 Name: "file-range", 345 URL: remoteURL + "file.txt", 346 Status: http.StatusPartialContent, 347 Range: "bytes=8-12", 348 Expected: `file1`, 349 }, { 350 Name: "bad-remote", 351 URL: "[notfoundremote:]/", 352 Status: http.StatusInternalServerError, 353 Expected: `{ 354 "error": "failed to make Fs: didn't find section in config file", 355 "input": null, 356 "path": "/", 357 "status": 500 358 } 359 `, 360 }} 361 opt := newTestOpt() 362 opt.Serve = true 363 opt.Files = testFs 364 testServer(t, tests, &opt) 365 } 366 367 func TestRC(t *testing.T) { 368 tests := []testRun{{ 369 Name: "rc-root", 370 URL: "", 371 Method: "POST", 372 Status: http.StatusNotFound, 373 Expected: `{ 374 "error": "couldn't find method \"\"", 375 "input": {}, 376 "path": "", 377 "status": 404 378 } 379 `, 380 }, { 381 Name: "rc-noop", 382 URL: "rc/noop", 383 Method: "POST", 384 Status: http.StatusOK, 385 Expected: "{}\n", 386 }, { 387 Name: "rc-error", 388 URL: "rc/error", 389 Method: "POST", 390 Status: http.StatusInternalServerError, 391 Expected: `{ 392 "error": "arbitrary error on input map[]", 393 "input": {}, 394 "path": "rc/error", 395 "status": 500 396 } 397 `, 398 }, { 399 Name: "core-gc", 400 URL: "core/gc", // returns nil, nil so check it is made into {} 401 Method: "POST", 402 Status: http.StatusOK, 403 Expected: "{}\n", 404 }, { 405 Name: "url-params", 406 URL: "rc/noop?param1=potato¶m2=sausage", 407 Method: "POST", 408 Status: http.StatusOK, 409 Expected: `{ 410 "param1": "potato", 411 "param2": "sausage" 412 } 413 `, 414 }, { 415 Name: "json", 416 URL: "rc/noop", 417 Method: "POST", 418 Body: `{ "param1":"string", "param2":true }`, 419 ContentType: "application/json", 420 Status: http.StatusOK, 421 Expected: `{ 422 "param1": "string", 423 "param2": true 424 } 425 `, 426 }, { 427 Name: "json-and-url-params", 428 URL: "rc/noop?param1=potato¶m2=sausage", 429 Method: "POST", 430 Body: `{ "param1":"string", "param3":true }`, 431 ContentType: "application/json", 432 Status: http.StatusOK, 433 Expected: `{ 434 "param1": "string", 435 "param2": "sausage", 436 "param3": true 437 } 438 `, 439 }, { 440 Name: "json-bad", 441 URL: "rc/noop?param1=potato¶m2=sausage", 442 Method: "POST", 443 Body: `{ param1":"string", "param3":true }`, 444 ContentType: "application/json", 445 Status: http.StatusBadRequest, 446 Expected: `{ 447 "error": "failed to read input JSON: invalid character 'p' looking for beginning of object key string", 448 "input": { 449 "param1": "potato", 450 "param2": "sausage" 451 }, 452 "path": "rc/noop", 453 "status": 400 454 } 455 `, 456 }, { 457 Name: "form", 458 URL: "rc/noop", 459 Method: "POST", 460 Body: `param1=string¶m2=true`, 461 ContentType: "application/x-www-form-urlencoded", 462 Status: http.StatusOK, 463 Expected: `{ 464 "param1": "string", 465 "param2": "true" 466 } 467 `, 468 }, { 469 Name: "form-and-url-params", 470 URL: "rc/noop?param1=potato¶m2=sausage", 471 Method: "POST", 472 Body: `param1=string¶m3=true`, 473 ContentType: "application/x-www-form-urlencoded", 474 Status: http.StatusOK, 475 Expected: `{ 476 "param1": "potato", 477 "param2": "sausage", 478 "param3": "true" 479 } 480 `, 481 }, { 482 Name: "form-bad", 483 URL: "rc/noop?param1=potato¶m2=sausage", 484 Method: "POST", 485 Body: `%zz`, 486 ContentType: "application/x-www-form-urlencoded", 487 Status: http.StatusBadRequest, 488 Expected: `{ 489 "error": "failed to parse form/URL parameters: invalid URL escape \"%zz\"", 490 "input": null, 491 "path": "rc/noop", 492 "status": 400 493 } 494 `, 495 }} 496 opt := newTestOpt() 497 opt.Serve = true 498 opt.Files = testFs 499 testServer(t, tests, &opt) 500 } 501 502 func TestRCWithAuth(t *testing.T) { 503 tests := []testRun{{ 504 Name: "core-command", 505 URL: "core/command", 506 Method: "POST", 507 Body: `command=version`, 508 ContentType: "application/x-www-form-urlencoded", 509 Status: http.StatusOK, 510 Expected: fmt.Sprintf(`{ 511 "error": false, 512 "result": "rclone %s\n" 513 } 514 `, fs.Version), 515 }, { 516 Name: "core-command-bad-returnType", 517 URL: "core/command", 518 Method: "POST", 519 Body: `command=version&returnType=POTATO`, 520 ContentType: "application/x-www-form-urlencoded", 521 Status: http.StatusInternalServerError, 522 Expected: `{ 523 "error": "unknown returnType \"POTATO\"", 524 "input": { 525 "command": "version", 526 "returnType": "POTATO" 527 }, 528 "path": "core/command", 529 "status": 500 530 } 531 `, 532 }, { 533 Name: "core-command-stream", 534 URL: "core/command", 535 Method: "POST", 536 Body: `command=version&returnType=STREAM`, 537 ContentType: "application/x-www-form-urlencoded", 538 Status: http.StatusOK, 539 Expected: fmt.Sprintf(`rclone %s 540 {} 541 `, fs.Version), 542 }, { 543 Name: "core-command-stream-error", 544 URL: "core/command", 545 Method: "POST", 546 Body: `command=unknown_command&returnType=STREAM`, 547 ContentType: "application/x-www-form-urlencoded", 548 Status: http.StatusOK, 549 Expected: fmt.Sprintf(`rclone %s 550 Unknown command 551 { 552 "error": "exit status 1", 553 "input": { 554 "command": "unknown_command", 555 "returnType": "STREAM" 556 }, 557 "path": "core/command", 558 "status": 500 559 } 560 `, fs.Version), 561 }} 562 opt := newTestOpt() 563 opt.Serve = true 564 opt.Files = testFs 565 opt.NoAuth = true 566 testServer(t, tests, &opt) 567 } 568 569 func TestMetrics(t *testing.T) { 570 stats := accounting.GlobalStats() 571 tests := makeMetricsTestCases(stats) 572 opt := newTestOpt() 573 opt.EnableMetrics = true 574 testServer(t, tests, &opt) 575 576 // Test changing a couple options 577 stats.Bytes(500) 578 for i := 0; i < 30; i++ { 579 require.NoError(t, stats.DeleteFile(context.Background(), 0)) 580 } 581 stats.Errors(2) 582 stats.Bytes(324) 583 584 tests = makeMetricsTestCases(stats) 585 testServer(t, tests, &opt) 586 } 587 588 func makeMetricsTestCases(stats *accounting.StatsInfo) (tests []testRun) { 589 tests = []testRun{{ 590 Name: "Bytes Transferred Metric", 591 URL: "/metrics", 592 Method: "GET", 593 Status: http.StatusOK, 594 Contains: regexp.MustCompile(fmt.Sprintf("rclone_bytes_transferred_total %d", stats.GetBytes())), 595 }, { 596 Name: "Checked Files Metric", 597 URL: "/metrics", 598 Method: "GET", 599 Status: http.StatusOK, 600 Contains: regexp.MustCompile(fmt.Sprintf("rclone_checked_files_total %d", stats.GetChecks())), 601 }, { 602 Name: "Errors Metric", 603 URL: "/metrics", 604 Method: "GET", 605 Status: http.StatusOK, 606 Contains: regexp.MustCompile(fmt.Sprintf("rclone_errors_total %d", stats.GetErrors())), 607 }, { 608 Name: "Deleted Files Metric", 609 URL: "/metrics", 610 Method: "GET", 611 Status: http.StatusOK, 612 Contains: regexp.MustCompile(fmt.Sprintf("rclone_files_deleted_total %d", stats.GetDeletes())), 613 }, { 614 Name: "Files Transferred Metric", 615 URL: "/metrics", 616 Method: "GET", 617 Status: http.StatusOK, 618 Contains: regexp.MustCompile(fmt.Sprintf("rclone_files_transferred_total %d", stats.GetTransfers())), 619 }, 620 } 621 return 622 } 623 624 var matchRemoteDirListing = regexp.MustCompile(`<title>Directory listing of /</title>`) 625 626 func TestServingRoot(t *testing.T) { 627 tests := []testRun{{ 628 Name: "rootlist", 629 URL: "*", 630 Status: http.StatusOK, 631 Contains: matchRemoteDirListing, 632 }} 633 opt := newTestOpt() 634 opt.Serve = true 635 opt.Files = testFs 636 testServer(t, tests, &opt) 637 } 638 639 func TestServingRootNoFiles(t *testing.T) { 640 tests := []testRun{{ 641 Name: "rootlist", 642 URL: "", 643 Status: http.StatusOK, 644 Contains: matchRemoteDirListing, 645 }} 646 opt := newTestOpt() 647 opt.Serve = true 648 opt.Files = "" 649 testServer(t, tests, &opt) 650 } 651 652 func TestNoFiles(t *testing.T) { 653 tests := []testRun{{ 654 Name: "file", 655 URL: "file.txt", 656 Status: http.StatusNotFound, 657 Expected: "Not Found\n", 658 }, { 659 Name: "dir", 660 URL: "dir/", 661 Status: http.StatusNotFound, 662 Expected: "Not Found\n", 663 }} 664 opt := newTestOpt() 665 opt.Serve = true 666 opt.Files = "" 667 testServer(t, tests, &opt) 668 } 669 670 func TestNoServe(t *testing.T) { 671 tests := []testRun{{ 672 Name: "file", 673 URL: remoteURL + "file.txt", 674 Status: http.StatusNotFound, 675 Expected: "404 page not found\n", 676 }, { 677 Name: "dir", 678 URL: remoteURL + "dir/", 679 Status: http.StatusNotFound, 680 Expected: "404 page not found\n", 681 }} 682 opt := newTestOpt() 683 opt.Serve = false 684 opt.Files = testFs 685 testServer(t, tests, &opt) 686 } 687 688 func TestAuthRequired(t *testing.T) { 689 tests := []testRun{{ 690 Name: "auth", 691 URL: "rc/noopauth", 692 Method: "POST", 693 Body: `{}`, 694 ContentType: "application/javascript", 695 Status: http.StatusForbidden, 696 Expected: `{ 697 "error": "authentication must be set up on the rc server to use \"rc/noopauth\" or the --rc-no-auth flag must be in use", 698 "input": {}, 699 "path": "rc/noopauth", 700 "status": 403 701 } 702 `, 703 }} 704 opt := newTestOpt() 705 opt.Serve = false 706 opt.Files = "" 707 opt.NoAuth = false 708 testServer(t, tests, &opt) 709 } 710 711 func TestNoAuth(t *testing.T) { 712 tests := []testRun{{ 713 Name: "auth", 714 URL: "rc/noopauth", 715 Method: "POST", 716 Body: `{}`, 717 ContentType: "application/javascript", 718 Status: http.StatusOK, 719 Expected: "{}\n", 720 }} 721 opt := newTestOpt() 722 opt.Serve = false 723 opt.Files = "" 724 opt.NoAuth = true 725 testServer(t, tests, &opt) 726 } 727 728 func TestWithUserPass(t *testing.T) { 729 tests := []testRun{{ 730 Name: "authMissing", 731 URL: "rc/noopauth", 732 Method: "POST", 733 Body: `{}`, 734 ContentType: "application/javascript", 735 Status: http.StatusUnauthorized, 736 Expected: "401 Unauthorized\n", 737 }, { 738 Name: "authWrong", 739 URL: "rc/noopauth", 740 Method: "POST", 741 Body: `{}`, 742 ContentType: "application/javascript", 743 Status: http.StatusUnauthorized, 744 Expected: "401 Unauthorized\n", 745 User: "user1", 746 Pass: "pass2", 747 }, { 748 Name: "authOK", 749 URL: "rc/noopauth", 750 Method: "POST", 751 Body: `{}`, 752 ContentType: "application/javascript", 753 Status: http.StatusOK, 754 Expected: "{}\n", 755 User: "user", 756 Pass: "pass", 757 }} 758 opt := newTestOpt() 759 opt.Serve = false 760 opt.Files = "" 761 opt.NoAuth = false 762 opt.Auth.BasicUser = "user" 763 opt.Auth.BasicPass = "pass" 764 testServer(t, tests, &opt) 765 } 766 767 func TestRCAsync(t *testing.T) { 768 tests := []testRun{{ 769 Name: "ok", 770 URL: "rc/noop", 771 Method: "POST", 772 ContentType: "application/json", 773 Body: `{ "_async":true }`, 774 Status: http.StatusOK, 775 Contains: regexp.MustCompile(`(?s)\{.*\"jobid\":.*\}`), 776 }, { 777 Name: "bad", 778 URL: "rc/noop", 779 Method: "POST", 780 ContentType: "application/json", 781 Body: `{ "_async":"truthy" }`, 782 Status: http.StatusBadRequest, 783 Expected: `{ 784 "error": "couldn't parse key \"_async\" (truthy) as bool: strconv.ParseBool: parsing \"truthy\": invalid syntax", 785 "input": { 786 "_async": "truthy" 787 }, 788 "path": "rc/noop", 789 "status": 400 790 } 791 `, 792 }} 793 opt := newTestOpt() 794 opt.Serve = true 795 opt.Files = "" 796 testServer(t, tests, &opt) 797 } 798 799 // Check the debug handlers are attached 800 func TestRCDebug(t *testing.T) { 801 tests := []testRun{{ 802 Name: "index", 803 URL: "debug/pprof/", 804 Method: "GET", 805 ContentType: "text/html", 806 Status: http.StatusOK, 807 Contains: regexp.MustCompile(`Types of profiles available`), 808 }, { 809 Name: "goroutines", 810 URL: "debug/pprof/goroutine?debug=1", 811 Method: "GET", 812 ContentType: "text/html", 813 Status: http.StatusOK, 814 Contains: regexp.MustCompile(`goroutine profile`), 815 }} 816 opt := newTestOpt() 817 opt.Serve = true 818 opt.Files = "" 819 testServer(t, tests, &opt) 820 } 821 822 func TestServeModTime(t *testing.T) { 823 for file, mtime := range map[string]time.Time{ 824 "dir": time.Date(2023, 4, 12, 21, 15, 17, 0, time.UTC), 825 "modtime.txt": time.Date(2021, 1, 18, 5, 2, 28, 0, time.UTC), 826 } { 827 path := filepath.Join(testFs, "modtime", file) 828 err := os.Chtimes(path, mtime, mtime) 829 require.NoError(t, err) 830 } 831 832 opt := newTestOpt() 833 opt.Serve = true 834 opt.Template.Path = "testdata/golden/testmodtime.html" 835 836 tests := []testRun{{ 837 Name: "modtime", 838 Method: "GET", 839 URL: remoteURL + "modtime/", 840 Status: http.StatusOK, 841 Expected: "* dir/ - 2023-04-12T21:15:17Z\n* modtime.txt - 2021-01-18T05:02:28Z\n", 842 }} 843 testServer(t, tests, &opt) 844 845 opt.ServeNoModTime = true 846 tests = []testRun{{ 847 Name: "no modtime", 848 Method: "GET", 849 URL: remoteURL + "modtime/", 850 Status: http.StatusOK, 851 Expected: "* dir/ - 0001-01-01T00:00:00Z\n* modtime.txt - 0001-01-01T00:00:00Z\n", 852 }} 853 testServer(t, tests, &opt) 854 } 855 856 func TestContentTypeJSON(t *testing.T) { 857 tests := []testRun{ 858 { 859 Name: "Check Content-Type for JSON response", 860 URL: "rc/noop", 861 Method: "POST", 862 Body: `{}`, 863 ContentType: "application/json", 864 Status: http.StatusOK, 865 Expected: "{}\n", 866 Headers: map[string]string{ 867 "Content-Type": "application/json", 868 }, 869 }, 870 { 871 Name: "Check Content-Type for JSON error response", 872 URL: "rc/error", 873 Method: "POST", 874 Body: `{}`, 875 ContentType: "application/json", 876 Status: http.StatusInternalServerError, 877 Expected: `{ 878 "error": "arbitrary error on input map[]", 879 "input": {}, 880 "path": "rc/error", 881 "status": 500 882 } 883 `, 884 Headers: map[string]string{ 885 "Content-Type": "application/json", 886 }, 887 }, 888 } 889 opt := newTestOpt() 890 testServer(t, tests, &opt) 891 } 892 893 func normalizeJSON(t *testing.T, jsonStr string) string { 894 var jsonObj map[string]interface{} 895 err := json.Unmarshal([]byte(jsonStr), &jsonObj) 896 require.NoError(t, err, "JSON unmarshalling failed") 897 normalizedJSON, err := json.Marshal(jsonObj) 898 require.NoError(t, err, "JSON marshalling failed") 899 return string(normalizedJSON) 900 }