github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/mw_js_plugin_test.go (about) 1 package gateway 2 3 import ( 4 "bytes" 5 "io" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "regexp" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/stretchr/testify/assert" 16 17 "github.com/sirupsen/logrus" 18 prefixed "github.com/x-cray/logrus-prefixed-formatter" 19 20 "github.com/TykTechnologies/tyk/apidef" 21 "github.com/TykTechnologies/tyk/config" 22 logger "github.com/TykTechnologies/tyk/log" 23 "github.com/TykTechnologies/tyk/test" 24 ) 25 26 func TestJSVMLogs(t *testing.T) { 27 var buf bytes.Buffer 28 log := logrus.New() 29 log.Out = &buf 30 log.Formatter = new(prefixed.TextFormatter) 31 32 jsvm := JSVM{} 33 jsvm.Init(nil, logrus.NewEntry(log)) 34 35 jsvm.RawLog = logrus.New() 36 jsvm.RawLog.Out = &buf 37 jsvm.RawLog.Formatter = new(logger.RawFormatter) 38 39 const in = ` 40 log("foo") 41 log('{"x": "y"}') 42 rawlog("foo") 43 rawlog('{"x": "y"}') 44 ` 45 46 want := []string{ 47 `time=TIME level=info msg=foo prefix=jsvm type=log-msg`, 48 `time=TIME level=info msg="{"x": "y"}" prefix=jsvm type=log-msg`, 49 `foo`, 50 `{"x": "y"}`, 51 } 52 if _, err := jsvm.VM.Run(in); err != nil { 53 t.Fatalf("failed to run js: %v", err) 54 } 55 got := strings.Split(strings.Trim(buf.String(), "\n"), "\n") 56 i := 0 57 timeRe := regexp.MustCompile(`time="[^"]*"`) 58 for _, line := range got { 59 if i >= len(want) { 60 t.Logf("too many lines") 61 t.Fail() 62 break 63 } 64 s := timeRe.ReplaceAllString(line, "time=TIME") 65 if s != line && !strings.Contains(s, "type=log-msg") { 66 continue // log line from elsewhere (async) 67 } 68 if s != want[i] { 69 t.Logf("%s != %s", s, want[i]) 70 t.Fail() 71 } 72 i++ 73 } 74 } 75 76 func TestJSVMBody(t *testing.T) { 77 dynMid := &DynamicMiddleware{ 78 BaseMiddleware: BaseMiddleware{ 79 Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, 80 }, 81 MiddlewareClassName: "leakMid", 82 Pre: true, 83 } 84 body := "foô \uffff \u0000 \xff bàr" 85 req := httptest.NewRequest("GET", "/foo", strings.NewReader(body)) 86 jsvm := JSVM{} 87 jsvm.Init(nil, logrus.NewEntry(log)) 88 89 const js = ` 90 var leakMid = new TykJS.TykMiddleware.NewMiddleware({}) 91 92 leakMid.NewProcessRequest(function(request, session) { 93 request.Body += " appended" 94 return leakMid.ReturnData(request, session.meta_data) 95 });` 96 if _, err := jsvm.VM.Run(js); err != nil { 97 t.Fatalf("failed to set up js plugin: %v", err) 98 } 99 dynMid.Spec.JSVM = jsvm 100 dynMid.ProcessRequest(nil, req, nil) 101 102 bs, err := ioutil.ReadAll(req.Body) 103 if err != nil { 104 t.Fatalf("failed to read final body: %v", err) 105 } 106 want := body + " appended" 107 if got := string(bs); want != got { 108 t.Fatalf("JS plugin broke non-UTF8 body %q into %q", 109 want, got) 110 } 111 } 112 113 func TestJSVMProcessTimeout(t *testing.T) { 114 dynMid := &DynamicMiddleware{ 115 BaseMiddleware: BaseMiddleware{ 116 Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, 117 }, 118 MiddlewareClassName: "leakMid", 119 Pre: true, 120 } 121 req := httptest.NewRequest("GET", "/foo", strings.NewReader("body")) 122 jsvm := JSVM{} 123 jsvm.Init(nil, logrus.NewEntry(log)) 124 jsvm.Timeout = time.Millisecond 125 126 // this js plugin just loops forever, keeping Otto at 100% CPU 127 // usage and running forever. 128 const js = ` 129 var leakMid = new TykJS.TykMiddleware.NewMiddleware({}) 130 131 leakMid.NewProcessRequest(function(request, session) { 132 while (true) { 133 } 134 return leakMid.ReturnData(request, session.meta_data) 135 });` 136 if _, err := jsvm.VM.Run(js); err != nil { 137 t.Fatalf("failed to set up js plugin: %v", err) 138 } 139 dynMid.Spec.JSVM = jsvm 140 141 done := make(chan bool) 142 go func() { 143 dynMid.ProcessRequest(nil, req, nil) 144 done <- true 145 }() 146 select { 147 case <-done: 148 case <-time.After(time.Second): 149 t.Fatal("js vm wasn't killed after its timeout") 150 } 151 } 152 153 func TestJSVMConfigData(t *testing.T) { 154 spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} 155 spec.ConfigData = map[string]interface{}{ 156 "foo": "bar", 157 } 158 const js = ` 159 var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({}) 160 161 testJSVMData.NewProcessRequest(function(request, session, spec) { 162 request.SetHeaders["data-foo"] = spec.config_data.foo 163 return testJSVMData.ReturnData(request, {}) 164 });` 165 dynMid := &DynamicMiddleware{ 166 BaseMiddleware: BaseMiddleware{Spec: spec, Proxy: nil}, 167 MiddlewareClassName: "testJSVMData", 168 Pre: true, 169 } 170 jsvm := JSVM{} 171 jsvm.Init(nil, logrus.NewEntry(log)) 172 if _, err := jsvm.VM.Run(js); err != nil { 173 t.Fatalf("failed to set up js plugin: %v", err) 174 } 175 dynMid.Spec.JSVM = jsvm 176 177 r := TestReq(t, "GET", "/v1/test-data", nil) 178 dynMid.ProcessRequest(nil, r, nil) 179 if want, got := "bar", r.Header.Get("data-foo"); want != got { 180 t.Fatalf("wanted header to be %q, got %q", want, got) 181 } 182 } 183 184 func TestJSVMUserCore(t *testing.T) { 185 spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} 186 const js = ` 187 var testJSVMCore = new TykJS.TykMiddleware.NewMiddleware({}) 188 189 testJSVMCore.NewProcessRequest(function(request, session, config) { 190 request.SetHeaders["global"] = globalVar 191 return testJSVMCore.ReturnData(request, {}) 192 });` 193 dynMid := &DynamicMiddleware{ 194 BaseMiddleware: BaseMiddleware{Spec: spec, Proxy: nil}, 195 MiddlewareClassName: "testJSVMCore", 196 Pre: true, 197 } 198 tfile, err := ioutil.TempFile("", "tykjs") 199 if err != nil { 200 t.Fatal(err) 201 } 202 if _, err := io.WriteString(tfile, `var globalVar = "globalValue"`); err != nil { 203 t.Fatal(err) 204 } 205 globalConf := config.Global() 206 old := globalConf.TykJSPath 207 globalConf.TykJSPath = tfile.Name() 208 config.SetGlobal(globalConf) 209 defer func() { 210 globalConf.TykJSPath = old 211 config.SetGlobal(globalConf) 212 }() 213 jsvm := JSVM{} 214 jsvm.Init(nil, logrus.NewEntry(log)) 215 if _, err := jsvm.VM.Run(js); err != nil { 216 t.Fatalf("failed to set up js plugin: %v", err) 217 } 218 dynMid.Spec.JSVM = jsvm 219 220 r := TestReq(t, "GET", "/foo", nil) 221 dynMid.ProcessRequest(nil, r, nil) 222 223 if want, got := "globalValue", r.Header.Get("global"); want != got { 224 t.Fatalf("wanted header to be %q, got %q", want, got) 225 } 226 } 227 228 func TestJSVMRequestScheme(t *testing.T) { 229 dynMid := &DynamicMiddleware{ 230 BaseMiddleware: BaseMiddleware{ 231 Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, 232 }, 233 MiddlewareClassName: "leakMid", 234 Pre: true, 235 } 236 req := httptest.NewRequest("GET", "/foo", nil) 237 jsvm := JSVM{} 238 jsvm.Init(nil, logrus.NewEntry(log)) 239 240 const js = ` 241 var leakMid = new TykJS.TykMiddleware.NewMiddleware({}) 242 leakMid.NewProcessRequest(function(request, session) { 243 var test = request.Scheme += " appended" 244 var responseObject = { 245 Body: test, 246 Code: 200 247 } 248 return leakMid.ReturnData(responseObject, session.meta_data) 249 });` 250 if _, err := jsvm.VM.Run(js); err != nil { 251 t.Fatalf("failed to set up js plugin: %v", err) 252 } 253 dynMid.Spec.JSVM = jsvm 254 dynMid.ProcessRequest(nil, req, nil) 255 256 bs, err := ioutil.ReadAll(req.Body) 257 if err != nil { 258 t.Fatalf("failed to read final body: %v", err) 259 } 260 want := "http" + " appended" 261 if got := string(bs); want != got { 262 t.Fatalf("JS plugin broke non-UTF8 body %q into %q", 263 want, got) 264 } 265 } 266 267 func TestTykMakeHTTPRequest(t *testing.T) { 268 ts := StartTest() 269 defer ts.Close() 270 271 bundle := RegisterBundle("jsvm_make_http_request", map[string]string{ 272 "manifest.json": ` 273 { 274 "file_list": [], 275 "custom_middleware": { 276 "driver": "otto", 277 "pre": [{ 278 "name": "testTykMakeHTTPRequest", 279 "path": "middleware.js" 280 }] 281 } 282 } 283 `, 284 "middleware.js": ` 285 var testTykMakeHTTPRequest = new TykJS.TykMiddleware.NewMiddleware({}) 286 287 testTykMakeHTTPRequest.NewProcessRequest(function(request, session, spec) { 288 var newRequest = { 289 "Method": "GET", 290 "Headers": {"Accept": "application/json"}, 291 "Domain": spec.config_data.base_url, 292 "Resource": "/api/get?param1=dummy" 293 } 294 295 var resp = TykMakeHttpRequest(JSON.stringify(newRequest)); 296 var usableResponse = JSON.parse(resp); 297 298 if(usableResponse.Code > 400) { 299 request.ReturnOverrides.ResponseCode = usableResponse.code 300 request.ReturnOverrides.ResponseError = "error" 301 } 302 303 request.Body = usableResponse.Body 304 305 return testTykMakeHTTPRequest.ReturnData(request, {}) 306 }); 307 `}) 308 309 t.Run("Existing endpoint", func(t *testing.T) { 310 BuildAndLoadAPI(func(spec *APISpec) { 311 spec.Proxy.ListenPath = "/sample" 312 spec.ConfigData = map[string]interface{}{ 313 "base_url": ts.URL, 314 } 315 spec.CustomMiddlewareBundle = bundle 316 }, func(spec *APISpec) { 317 spec.Proxy.ListenPath = "/api" 318 }) 319 320 ts.Run(t, test.TestCase{Path: "/sample", Code: 200}) 321 }) 322 323 t.Run("Nonexistent endpoint", func(t *testing.T) { 324 BuildAndLoadAPI(func(spec *APISpec) { 325 spec.Proxy.ListenPath = "/sample" 326 spec.ConfigData = map[string]interface{}{ 327 "base_url": ts.URL, 328 } 329 spec.CustomMiddlewareBundle = bundle 330 }) 331 332 ts.Run(t, test.TestCase{Path: "/sample", Code: 404}) 333 }) 334 335 t.Run("Endpoint with query", func(t *testing.T) { 336 BuildAndLoadAPI(func(spec *APISpec) { 337 spec.Proxy.ListenPath = "/sample" 338 spec.ConfigData = map[string]interface{}{ 339 "base_url": ts.URL, 340 } 341 spec.CustomMiddlewareBundle = bundle 342 }, func(spec *APISpec) { 343 spec.Proxy.ListenPath = "/api" 344 }) 345 346 ts.Run(t, test.TestCase{Path: "/sample", BodyMatch: `/api/get\?param1=dummy`, Code: 200}) 347 }) 348 349 t.Run("Endpoint with skip cleaning", func(t *testing.T) { 350 ts.Close() 351 globalConf := config.Global() 352 globalConf.HttpServerOptions.SkipURLCleaning = true 353 globalConf.HttpServerOptions.OverrideDefaults = true 354 config.SetGlobal(globalConf) 355 356 prevSkipClean := defaultTestConfig.HttpServerOptions.OverrideDefaults && 357 defaultTestConfig.HttpServerOptions.SkipURLCleaning 358 testServerRouter.SkipClean(true) 359 defer testServerRouter.SkipClean(prevSkipClean) 360 361 ts := StartTest() 362 defer ts.Close() 363 defer ResetTestConfig() 364 365 BuildAndLoadAPI(func(spec *APISpec) { 366 spec.Proxy.ListenPath = "/sample" 367 spec.ConfigData = map[string]interface{}{ 368 "base_url": ts.URL, 369 } 370 spec.CustomMiddlewareBundle = bundle 371 }, func(spec *APISpec) { 372 spec.Proxy.ListenPath = "/api" 373 }) 374 375 ts.Run(t, test.TestCase{Path: "/sample/99999-XXXX+%2F%2F+dog+9+fff%C3%A9o+party", BodyMatch: `URI":"/sample/99999-XXXX\+%2F%2F\+dog\+9\+fff%C3%A9o\+party"`, Code: 200}) 376 }) 377 } 378 379 func TestJSVMBase64(t *testing.T) { 380 jsvm := JSVM{} 381 jsvm.Init(nil, logrus.NewEntry(log)) 382 383 inputString := "teststring" 384 inputB64 := "dGVzdHN0cmluZw==" 385 jwtPayload := "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" 386 decodedJwtPayload := `{"sub":"1234567890","name":"John Doe","iat":1516239022}` 387 388 t.Run("b64dec with simple string input", func(t *testing.T) { 389 v, err := jsvm.VM.Run(`b64dec("` + inputB64 + `")`) 390 if err != nil { 391 t.Fatalf("b64dec call failed: %s", err.Error()) 392 } 393 if s := v.String(); s != inputString { 394 t.Fatalf("wanted '%s', got '%s'", inputString, s) 395 } 396 }) 397 398 t.Run("b64dec with a JWT payload", func(t *testing.T) { 399 v, err := jsvm.VM.Run(`b64dec("` + jwtPayload + `")`) 400 if err != nil { 401 t.Fatalf("b64dec call failed: %s", err.Error()) 402 } 403 if s := v.String(); s != decodedJwtPayload { 404 t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s) 405 } 406 }) 407 408 t.Run("b64enc with simple string input", func(t *testing.T) { 409 v, err := jsvm.VM.Run(`b64enc("` + inputString + `")`) 410 if err != nil { 411 t.Fatalf("b64enc call failed: %s", err.Error()) 412 } 413 if s := v.String(); s != inputB64 { 414 t.Fatalf("wanted '%s', got '%s'", inputB64, s) 415 } 416 }) 417 418 t.Run("rawb64dec with simple string input", func(t *testing.T) { 419 v, err := jsvm.VM.Run(`rawb64dec("` + jwtPayload + `")`) 420 if err != nil { 421 t.Fatalf("rawb64dec call failed: %s", err.Error()) 422 } 423 if s := v.String(); s != decodedJwtPayload { 424 t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s) 425 } 426 }) 427 428 t.Run("rawb64enc with simple string input", func(t *testing.T) { 429 jsvm.VM.Set("input", decodedJwtPayload) 430 v, err := jsvm.VM.Run(`rawb64enc(input)`) 431 if err != nil { 432 t.Fatalf("rawb64enc call failed: %s", err.Error()) 433 } 434 if s := v.String(); s != jwtPayload { 435 t.Fatalf("wanted '%s', got '%s'", jwtPayload, s) 436 } 437 }) 438 } 439 440 func TestJSVMStagesRequest(t *testing.T) { 441 ts := StartTest() 442 defer ts.Close() 443 444 pre := `var pre = new TykJS.TykMiddleware.NewMiddleware({}); 445 446 pre.NewProcessRequest(function(request, session) { 447 // You can log to Tyk console output by calloing the built-in log() function: 448 log("Running sample PRE PROCESSOR JSVM middleware") 449 450 // Set headers in an outbound request 451 request.SetHeaders["Pre"] = "foobar"; 452 // Add or delete request parmeters, these are encoded for the request as needed. 453 request.AddParams["pre"] = "foobar"; 454 455 // You MUST return both the request and session metadata 456 return pre.ReturnData(request, {"pre": "foobar"}); 457 });` 458 459 post := `var post = new TykJS.TykMiddleware.NewMiddleware({}); 460 461 post.NewProcessRequest(function(request, session) { 462 // You can log to Tyk console output by calloing the built-in log() function: 463 log("Running sample POST PROCESSOR JSVM middleware") 464 465 // Set headers in an outbound request 466 request.SetHeaders["Post"] = "foobar"; 467 // Add or delete request parmeters, these are encoded for the request as needed. 468 request.AddParams["post"] = "foobar"; 469 470 // You MUST return both the request and session metadata 471 return post.ReturnData(request, {"post": "foobar"}); 472 });` 473 474 t.Run("Bundles", func(t *testing.T) { 475 bundle := RegisterBundle("jsvm_stages", map[string]string{ 476 "manifest.json": ` 477 { 478 "file_list": [], 479 "custom_middleware": { 480 "driver": "otto", 481 "pre": [{ 482 "name": "pre", 483 "path": "pre.js" 484 }], 485 "post": [{ 486 "name": "post", 487 "path": "post.js" 488 }] 489 } 490 } 491 `, 492 "pre.js": pre, 493 "post.js": post, 494 }) 495 496 BuildAndLoadAPI(func(spec *APISpec) { 497 spec.Proxy.ListenPath = "/test" 498 spec.CustomMiddlewareBundle = bundle 499 }) 500 501 ts.Run(t, []test.TestCase{ 502 {Path: "/test", Code: 200, BodyMatch: `"Pre":"foobar"`}, 503 {Path: "/test", Code: 200, BodyMatch: `"Post":"foobar"`}, 504 }...) 505 }) 506 507 t.Run("Files", func(t *testing.T) { 508 // Object names are forced to be "pre" and "post" 509 RegisterJSFileMiddleware("jsvm_file_test", map[string]string{ 510 "pre/pre.js": pre, 511 "post/post.js": post, 512 }) 513 514 BuildAndLoadAPI(func(spec *APISpec) { 515 spec.APIID = "jsvm_file_test" 516 spec.Proxy.ListenPath = "/test" 517 }) 518 519 ts.Run(t, []test.TestCase{ 520 {Path: "/test", Code: 200, BodyMatch: `"Pre":"foobar"`}, 521 {Path: "/test", Code: 200, BodyMatch: `"Post":"foobar"`}, 522 }...) 523 }) 524 525 t.Run("API definition", func(t *testing.T) { 526 // Write to non APIID folder 527 RegisterJSFileMiddleware("jsvm_api", map[string]string{ 528 "pre.js": pre, 529 "post.js": post, 530 }) 531 532 BuildAndLoadAPI(func(spec *APISpec) { 533 spec.Proxy.ListenPath = "/test" 534 spec.CustomMiddleware = apidef.MiddlewareSection{ 535 Pre: []apidef.MiddlewareDefinition{{ 536 Name: "pre", 537 Path: config.Global().MiddlewarePath + "/jsvm_api/pre.js", 538 }}, 539 Post: []apidef.MiddlewareDefinition{{ 540 Name: "post", 541 Path: config.Global().MiddlewarePath + "/jsvm_api/post.js", 542 }}, 543 } 544 }) 545 546 ts.Run(t, []test.TestCase{ 547 {Path: "/test", Code: 200, BodyMatch: `"Pre":"foobar"`}, 548 {Path: "/test", Code: 200, BodyMatch: `"Post":"foobar"`}, 549 }...) 550 }) 551 } 552 553 func TestMiniRequestObject_ReconstructParams(t *testing.T) { 554 const exampleURL = "http://example.com/get?b=1&c=2&a=3" 555 r, _ := http.NewRequest(http.MethodGet, exampleURL, nil) 556 mr := MiniRequestObject{} 557 558 t.Run("Don't touch queries if no change on params", func(t *testing.T) { 559 mr.ReconstructParams(r) 560 assert.Equal(t, exampleURL, r.URL.String()) 561 }) 562 563 t.Run("Update params", func(t *testing.T) { 564 mr.AddParams = map[string]string{ 565 "d": "4", 566 } 567 mr.DeleteParams = append(mr.DeleteHeaders, "b") 568 mr.ReconstructParams(r) 569 570 assert.Equal(t, url.Values{ 571 "a": []string{"3"}, 572 "c": []string{"2"}, 573 "d": []string{"4"}, 574 }, r.URL.Query()) 575 }) 576 } 577 578 func TestJSVM_Auth(t *testing.T) { 579 ts := StartTest() 580 defer ts.Close() 581 582 bundle := RegisterBundle("custom_auth", map[string]string{ 583 "manifest.json": `{ 584 "file_list": [ 585 "testmw.js" 586 ], 587 "custom_middleware": { 588 "pre": null, 589 "post": null, 590 "post_key_auth": null, 591 "auth_check": { 592 "name": "ottoAuthExample", 593 "path": "testmw.js", 594 "require_session": false 595 }, 596 "response": null, 597 "driver": "otto", 598 "id_extractor": { 599 "extract_from": "", 600 "extract_with": "", 601 "extractor_config": null 602 } 603 }, 604 "checksum": "65694908d609b14df0e280c1a95a8ca4", 605 "signature": "" 606 }`, 607 "testmw.js": `log("====> JS Auth initialising"); 608 609 var ottoAuthExample = new TykJS.TykMiddleware.NewMiddleware({}); 610 611 ottoAuthExample.NewProcessRequest(function(request, session) { 612 log("----> Running ottoAuthExample JSVM Auth Middleware") 613 614 var thisToken = request.Headers["Authorization"]; 615 616 if (thisToken == undefined) { 617 // no token at all? 618 request.ReturnOverrides.ResponseCode = 401 619 request.ReturnOverrides.ResponseError = 'Header missing (JS middleware)' 620 return ottoAuthExample.ReturnData(request, {}); 621 } 622 623 if (thisToken != "foobar") { 624 request.ReturnOverrides.ResponseCode = 401 625 request.ReturnOverrides.ResponseError = 'Not authorized (JS middleware)' 626 return ottoAuthExample.ReturnData(request, {}); 627 } 628 629 log("auth is ok") 630 631 var thisSession = { 632 "allowance": 100, 633 "rate": 100, 634 "per": 1, 635 "quota_max": -1, 636 "quota_renews": 1906121006, 637 "expires": 1906121006, 638 "access_rights": {} 639 }; 640 641 return ottoAuthExample.ReturnAuthData(request, thisSession); 642 }); 643 644 // Ensure init with a post-declaration log message 645 log("====> JS Auth initialised"); 646 `, 647 }) 648 BuildAndLoadAPI(func(spec *APISpec) { 649 spec.Proxy.ListenPath = "/sample" 650 spec.ConfigData = map[string]interface{}{ 651 "base_url": ts.URL, 652 } 653 spec.CustomMiddlewareBundle = bundle 654 spec.EnableCoProcessAuth = true 655 spec.UseKeylessAccess = false 656 }) 657 ts.Run(t, 658 test.TestCase{Path: "/sample", Code: http.StatusUnauthorized, BodyMatchFunc: func(b []byte) bool { 659 return strings.Contains(string(b), "Header missing (JS middleware)") 660 }}, 661 test.TestCase{Path: "/sample", Code: http.StatusUnauthorized, BodyMatchFunc: func(b []byte) bool { 662 return strings.Contains(string(b), "Not authorized (JS middleware)") 663 }, 664 Headers: map[string]string{"Authorization": "foo"}, 665 }, 666 test.TestCase{Path: "/sample", Code: http.StatusOK, Headers: map[string]string{ 667 "Authorization": "foobar", 668 }}, 669 ) 670 }