github.com/grpc-ecosystem/grpc-gateway/v2@v2.19.1/runtime/mux_test.go (about) 1 package runtime_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "strconv" 11 "strings" 12 "testing" 13 14 "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 15 "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" 16 "google.golang.org/grpc" 17 "google.golang.org/grpc/codes" 18 "google.golang.org/grpc/health/grpc_health_v1" 19 "google.golang.org/grpc/status" 20 ) 21 22 func TestMuxServeHTTP(t *testing.T) { 23 type stubPattern struct { 24 method string 25 ops []int 26 pool []string 27 verb string 28 } 29 for i, spec := range []struct { 30 patterns []stubPattern 31 32 reqMethod string 33 reqPath string 34 headers map[string]string 35 36 respStatus int 37 respContent string 38 39 disablePathLengthFallback bool 40 unescapingMode runtime.UnescapingMode 41 }{ 42 { 43 patterns: nil, 44 reqMethod: "GET", 45 reqPath: "/", 46 respStatus: http.StatusNotFound, 47 }, 48 { 49 patterns: []stubPattern{ 50 { 51 method: "GET", 52 ops: []int{int(utilities.OpLitPush), 0}, 53 pool: []string{"foo"}, 54 }, 55 }, 56 reqMethod: "GET", 57 reqPath: "/foo", 58 respStatus: http.StatusOK, 59 respContent: "GET /foo", 60 }, 61 { 62 patterns: []stubPattern{ 63 { 64 method: "GET", 65 ops: []int{int(utilities.OpLitPush), 0}, 66 pool: []string{"foo"}, 67 }, 68 }, 69 reqMethod: "GET", 70 reqPath: "/bar", 71 respStatus: http.StatusNotFound, 72 }, 73 { 74 patterns: []stubPattern{ 75 { 76 method: "GET", 77 ops: []int{int(utilities.OpPush), 0}, 78 }, 79 { 80 method: "GET", 81 ops: []int{int(utilities.OpLitPush), 0}, 82 pool: []string{"foo"}, 83 }, 84 }, 85 reqMethod: "GET", 86 reqPath: "/foo", 87 respStatus: http.StatusOK, 88 respContent: "GET /foo", 89 }, 90 { 91 patterns: []stubPattern{ 92 { 93 method: "GET", 94 ops: []int{int(utilities.OpLitPush), 0}, 95 pool: []string{"foo"}, 96 }, 97 { 98 method: "POST", 99 ops: []int{int(utilities.OpLitPush), 0}, 100 pool: []string{"foo"}, 101 }, 102 }, 103 reqMethod: "POST", 104 reqPath: "/foo", 105 respStatus: http.StatusOK, 106 respContent: "POST /foo", 107 }, 108 { 109 patterns: []stubPattern{ 110 { 111 method: "GET", 112 ops: []int{int(utilities.OpLitPush), 0}, 113 pool: []string{"foo"}, 114 }, 115 }, 116 reqMethod: "DELETE", 117 reqPath: "/foo", 118 respStatus: http.StatusNotImplemented, 119 }, 120 { 121 patterns: []stubPattern{ 122 { 123 method: "POST", 124 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 125 pool: []string{"foo", "id"}, 126 verb: "archive", 127 }, 128 }, 129 reqMethod: "DELETE", 130 reqPath: "/foo/bar:archive", 131 respStatus: http.StatusNotImplemented, 132 }, 133 { 134 patterns: []stubPattern{ 135 { 136 method: "GET", 137 ops: []int{int(utilities.OpLitPush), 0}, 138 pool: []string{"foo"}, 139 }, 140 }, 141 reqMethod: "POST", 142 reqPath: "/foo", 143 headers: map[string]string{ 144 "Content-Type": "application/x-www-form-urlencoded", 145 }, 146 respStatus: http.StatusOK, 147 respContent: "GET /foo", 148 }, 149 { 150 patterns: []stubPattern{ 151 { 152 method: "GET", 153 ops: []int{int(utilities.OpLitPush), 0}, 154 pool: []string{"foo"}, 155 }, 156 }, 157 reqMethod: "POST", 158 reqPath: "/foo", 159 headers: map[string]string{ 160 "Content-Type": "application/x-www-form-urlencoded", 161 }, 162 respStatus: http.StatusNotImplemented, 163 disablePathLengthFallback: true, 164 }, 165 { 166 patterns: []stubPattern{ 167 { 168 method: "GET", 169 ops: []int{int(utilities.OpLitPush), 0}, 170 pool: []string{"foo"}, 171 }, 172 { 173 method: "POST", 174 ops: []int{int(utilities.OpLitPush), 0}, 175 pool: []string{"foo"}, 176 }, 177 }, 178 reqMethod: "POST", 179 reqPath: "/foo", 180 headers: map[string]string{ 181 "Content-Type": "application/x-www-form-urlencoded", 182 }, 183 respStatus: http.StatusOK, 184 respContent: "POST /foo", 185 disablePathLengthFallback: true, 186 }, 187 { 188 patterns: []stubPattern{ 189 { 190 method: "GET", 191 ops: []int{int(utilities.OpLitPush), 0}, 192 pool: []string{"foo"}, 193 }, 194 { 195 method: "POST", 196 ops: []int{int(utilities.OpLitPush), 0}, 197 pool: []string{"foo"}, 198 }, 199 }, 200 reqMethod: "POST", 201 reqPath: "/foo", 202 headers: map[string]string{ 203 "Content-Type": "application/x-www-form-urlencoded", 204 "X-HTTP-Method-Override": "GET", 205 }, 206 respStatus: http.StatusOK, 207 respContent: "GET /foo", 208 }, 209 { 210 patterns: []stubPattern{ 211 { 212 method: "GET", 213 ops: []int{int(utilities.OpLitPush), 0}, 214 pool: []string{"foo"}, 215 }, 216 }, 217 reqMethod: "POST", 218 reqPath: "/foo", 219 headers: map[string]string{ 220 "Content-Type": "application/x-www-form-urlencoded", 221 }, 222 respStatus: http.StatusOK, 223 respContent: "GET /foo", 224 }, 225 { 226 patterns: []stubPattern{ 227 { 228 method: "DELETE", 229 ops: []int{int(utilities.OpLitPush), 0}, 230 pool: []string{"foo"}, 231 }, 232 { 233 method: "PUT", 234 ops: []int{int(utilities.OpLitPush), 0}, 235 pool: []string{"foo"}, 236 }, 237 { 238 method: "PATCH", 239 ops: []int{int(utilities.OpLitPush), 0}, 240 pool: []string{"foo"}, 241 }, 242 }, 243 reqMethod: "POST", 244 reqPath: "/foo", 245 headers: map[string]string{ 246 "Content-Type": "application/x-www-form-urlencoded", 247 }, 248 respStatus: http.StatusNotImplemented, 249 }, 250 { 251 patterns: []stubPattern{ 252 { 253 method: "GET", 254 ops: []int{int(utilities.OpLitPush), 0}, 255 pool: []string{"foo"}, 256 }, 257 }, 258 reqMethod: "POST", 259 reqPath: "/foo", 260 headers: map[string]string{ 261 "Content-Type": "application/json", 262 }, 263 respStatus: http.StatusNotImplemented, 264 }, 265 { 266 patterns: []stubPattern{ 267 { 268 method: "POST", 269 ops: []int{int(utilities.OpLitPush), 0}, 270 pool: []string{"foo"}, 271 verb: "bar", 272 }, 273 }, 274 reqMethod: "POST", 275 reqPath: "/foo:bar", 276 headers: map[string]string{ 277 "Content-Type": "application/json", 278 }, 279 respStatus: http.StatusOK, 280 respContent: "POST /foo:bar", 281 }, 282 { 283 patterns: []stubPattern{ 284 { 285 method: "GET", 286 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 287 pool: []string{"foo", "id"}, 288 }, 289 { 290 method: "GET", 291 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 292 pool: []string{"foo", "id"}, 293 verb: "verb", 294 }, 295 }, 296 reqMethod: "GET", 297 reqPath: "/foo/bar:verb", 298 headers: map[string]string{ 299 "Content-Type": "application/json", 300 }, 301 respStatus: http.StatusOK, 302 respContent: "GET /foo/{id=*}:verb", 303 }, 304 { 305 patterns: []stubPattern{ 306 { 307 method: "GET", 308 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 309 pool: []string{"foo", "id"}, 310 }, 311 }, 312 reqMethod: "GET", 313 reqPath: "/foo/bar", 314 headers: map[string]string{ 315 "Content-Type": "application/json", 316 }, 317 respStatus: http.StatusOK, 318 respContent: "GET /foo/{id=*}", 319 }, 320 { 321 patterns: []stubPattern{ 322 { 323 method: "GET", 324 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 325 pool: []string{"foo", "id"}, 326 }, 327 }, 328 reqMethod: "GET", 329 reqPath: "/foo/bar:123", 330 headers: map[string]string{ 331 "Content-Type": "application/json", 332 }, 333 respStatus: http.StatusOK, 334 respContent: "GET /foo/{id=*}", 335 }, 336 { 337 patterns: []stubPattern{ 338 { 339 method: "POST", 340 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 341 pool: []string{"foo", "id"}, 342 }, 343 { 344 method: "POST", 345 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 346 pool: []string{"foo", "id"}, 347 verb: "verb", 348 }, 349 }, 350 reqMethod: "POST", 351 reqPath: "/foo/bar:verb", 352 headers: map[string]string{ 353 "Content-Type": "application/json", 354 }, 355 respStatus: http.StatusOK, 356 respContent: "POST /foo/{id=*}:verb", 357 }, 358 { 359 patterns: []stubPattern{ 360 { 361 method: "GET", 362 ops: []int{int(utilities.OpLitPush), 0}, 363 pool: []string{"foo"}, 364 }, 365 }, 366 reqMethod: "POST", 367 reqPath: "foo", 368 headers: map[string]string{ 369 "Content-Type": "application/json", 370 }, 371 respStatus: http.StatusBadRequest, 372 }, 373 { 374 patterns: []stubPattern{ 375 { 376 method: "POST", 377 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 378 pool: []string{"foo", "id"}, 379 }, 380 { 381 method: "POST", 382 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1}, 383 pool: []string{"foo", "id"}, 384 verb: "verb:subverb", 385 }, 386 }, 387 reqMethod: "POST", 388 reqPath: "/foo/bar:verb:subverb", 389 headers: map[string]string{ 390 "Content-Type": "application/json", 391 }, 392 respStatus: http.StatusOK, 393 respContent: "POST /foo/{id=*}:verb:subverb", 394 }, 395 { 396 patterns: []stubPattern{ 397 { 398 method: "GET", 399 ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 1, int(utilities.OpCapture), 1, int(utilities.OpLitPush), 2}, 400 pool: []string{"foo", "id", "bar"}, 401 }, 402 }, 403 reqMethod: "POST", 404 reqPath: "/foo/404%2fwith%2Fspace/bar", 405 headers: map[string]string{ 406 "Content-Type": "application/json", 407 }, 408 respStatus: http.StatusNotFound, 409 unescapingMode: runtime.UnescapingModeLegacy, 410 }, 411 { 412 patterns: []stubPattern{ 413 { 414 method: "GET", 415 ops: []int{ 416 int(utilities.OpLitPush), 0, 417 int(utilities.OpPush), 0, 418 int(utilities.OpConcatN), 1, 419 int(utilities.OpCapture), 1, 420 int(utilities.OpLitPush), 2}, 421 pool: []string{"foo", "id", "bar"}, 422 }, 423 }, 424 reqMethod: "GET", 425 reqPath: "/foo/success%2fwith%2Fspace/bar", 426 headers: map[string]string{ 427 "Content-Type": "application/json", 428 }, 429 respStatus: http.StatusOK, 430 unescapingMode: runtime.UnescapingModeAllExceptReserved, 431 respContent: "GET /foo/{id=*}/bar", 432 }, 433 { 434 patterns: []stubPattern{ 435 { 436 method: "GET", 437 ops: []int{ 438 int(utilities.OpLitPush), 0, 439 int(utilities.OpPush), 0, 440 int(utilities.OpConcatN), 1, 441 int(utilities.OpCapture), 1, 442 int(utilities.OpLitPush), 2}, 443 pool: []string{"foo", "id", "bar"}, 444 }, 445 }, 446 reqMethod: "GET", 447 reqPath: "/foo/success%2fwith%2Fspace/bar", 448 headers: map[string]string{ 449 "Content-Type": "application/json", 450 }, 451 respStatus: http.StatusNotFound, 452 unescapingMode: runtime.UnescapingModeAllCharacters, 453 }, 454 { 455 patterns: []stubPattern{ 456 { 457 method: "GET", 458 ops: []int{ 459 int(utilities.OpLitPush), 0, 460 int(utilities.OpPush), 0, 461 int(utilities.OpConcatN), 1, 462 int(utilities.OpCapture), 1, 463 int(utilities.OpLitPush), 2}, 464 pool: []string{"foo", "id", "bar"}, 465 }, 466 }, 467 reqMethod: "GET", 468 reqPath: "/foo/success%2fwith%2Fspace/bar", 469 headers: map[string]string{ 470 "Content-Type": "application/json", 471 }, 472 respStatus: http.StatusNotFound, 473 unescapingMode: runtime.UnescapingModeLegacy, 474 }, 475 { 476 patterns: []stubPattern{ 477 { 478 method: "GET", 479 ops: []int{ 480 int(utilities.OpLitPush), 0, 481 int(utilities.OpPushM), 0, 482 int(utilities.OpConcatN), 1, 483 int(utilities.OpCapture), 1, 484 }, 485 pool: []string{"foo", "id", "bar"}, 486 }, 487 }, 488 reqMethod: "GET", 489 reqPath: "/foo/success%2fwith%2Fspace", 490 headers: map[string]string{ 491 "Content-Type": "application/json", 492 }, 493 respStatus: http.StatusOK, 494 unescapingMode: runtime.UnescapingModeAllExceptReserved, 495 respContent: "GET /foo/{id=**}", 496 }, 497 { 498 patterns: []stubPattern{ 499 { 500 method: "POST", 501 ops: []int{ 502 int(utilities.OpLitPush), 0, 503 int(utilities.OpLitPush), 1, 504 int(utilities.OpLitPush), 2, 505 int(utilities.OpPush), 0, 506 int(utilities.OpConcatN), 2, 507 int(utilities.OpCapture), 3, 508 }, 509 pool: []string{"api", "v1", "organizations", "name"}, 510 verb: "action", 511 }, 512 }, 513 reqMethod: "POST", 514 reqPath: "/api/v1/" + url.QueryEscape("organizations/foo") + ":action", 515 headers: map[string]string{ 516 "Content-Type": "application/json", 517 }, 518 respStatus: http.StatusOK, 519 unescapingMode: runtime.UnescapingModeAllCharacters, 520 respContent: "POST /api/v1/{name=organizations/*}:action", 521 }, 522 { 523 patterns: []stubPattern{ 524 { 525 method: "POST", 526 ops: []int{ 527 int(utilities.OpLitPush), 0, 528 int(utilities.OpLitPush), 1, 529 int(utilities.OpLitPush), 2, 530 }, 531 pool: []string{"api", "v1", "organizations"}, 532 verb: "verb", 533 }, 534 { 535 method: "POST", 536 ops: []int{ 537 int(utilities.OpLitPush), 0, 538 int(utilities.OpLitPush), 1, 539 int(utilities.OpLitPush), 2, 540 }, 541 pool: []string{"api", "v1", "organizations"}, 542 verb: "", 543 }, 544 { 545 method: "POST", 546 ops: []int{ 547 int(utilities.OpLitPush), 0, 548 int(utilities.OpLitPush), 1, 549 int(utilities.OpLitPush), 2, 550 }, 551 pool: []string{"api", "v1", "dummies"}, 552 verb: "verb", 553 }, 554 }, 555 reqMethod: "POST", 556 reqPath: "/api/v1/organizations:verb", 557 headers: map[string]string{ 558 "Content-Type": "application/json", 559 }, 560 respStatus: http.StatusOK, 561 unescapingMode: runtime.UnescapingModeAllCharacters, 562 respContent: "POST /api/v1/organizations:verb", 563 }, 564 } { 565 t.Run(strconv.Itoa(i), func(t *testing.T) { 566 var opts []runtime.ServeMuxOption 567 opts = append(opts, runtime.WithUnescapingMode(spec.unescapingMode)) 568 if spec.disablePathLengthFallback { 569 opts = append(opts, 570 runtime.WithDisablePathLengthFallback(), 571 ) 572 } 573 mux := runtime.NewServeMux(opts...) 574 for _, p := range spec.patterns { 575 func(p stubPattern) { 576 pat, err := runtime.NewPattern(1, p.ops, p.pool, p.verb) 577 if err != nil { 578 t.Fatalf("runtime.NewPattern(1, %#v, %#v, %q) failed with %v; want success", p.ops, p.pool, p.verb, err) 579 } 580 mux.Handle(p.method, pat, func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { 581 _, _ = fmt.Fprintf(w, "%s %s", p.method, pat.String()) 582 }) 583 }(p) 584 } 585 586 reqUrl := fmt.Sprintf("https://host.example%s", spec.reqPath) 587 ctx := context.Background() 588 r, err := http.NewRequestWithContext(ctx, spec.reqMethod, reqUrl, bytes.NewReader(nil)) 589 if err != nil { 590 t.Fatalf("http.NewRequest(%q, %q, nil) failed with %v; want success", spec.reqMethod, reqUrl, err) 591 } 592 for name, value := range spec.headers { 593 r.Header.Set(name, value) 594 } 595 w := httptest.NewRecorder() 596 mux.ServeHTTP(w, r) 597 598 if got, want := w.Code, spec.respStatus; got != want { 599 t.Errorf("w.Code = %d; want %d; patterns=%v; req=%v", got, want, spec.patterns, r) 600 } 601 if spec.respContent != "" { 602 if got, want := w.Body.String(), spec.respContent; got != want { 603 t.Errorf("w.Body = %q; want %q; patterns=%v; req=%v", got, want, spec.patterns, r) 604 } 605 } 606 }) 607 } 608 } 609 610 func TestServeHTTP_WithMethodOverrideAndFormParsing(t *testing.T) { 611 r := httptest.NewRequest("POST", "/foo", strings.NewReader("bar=hoge")) 612 r.Header.Set("Content-Type", "application/x-www-form-urlencoded") 613 r.Header.Set("X-HTTP-Method-Override", "GET") 614 w := httptest.NewRecorder() 615 616 runtime.NewServeMux().ServeHTTP(w, r) 617 618 if r.FormValue("bar") != "hoge" { 619 t.Error("form is not parsed") 620 } 621 } 622 623 var defaultHeaderMatcherTests = []struct { 624 name string 625 in string 626 outValue string 627 outValid bool 628 }{ 629 { 630 "permanent HTTP header should return prefixed", 631 "Accept", 632 "grpcgateway-Accept", 633 true, 634 }, 635 { 636 "key prefixed with MetadataHeaderPrefix should return without the prefix", 637 "Grpc-Metadata-Custom-Header", 638 "Custom-Header", 639 true, 640 }, 641 { 642 "non-permanent HTTP header key without prefix should not return", 643 "Custom-Header", 644 "", 645 false, 646 }, 647 } 648 649 func TestDefaultHeaderMatcher(t *testing.T) { 650 for _, tt := range defaultHeaderMatcherTests { 651 t.Run(tt.name, func(t *testing.T) { 652 out, valid := runtime.DefaultHeaderMatcher(tt.in) 653 if out != tt.outValue { 654 t.Errorf("got %v, want %v", out, tt.outValue) 655 } 656 if valid != tt.outValid { 657 t.Errorf("got %v, want %v", valid, tt.outValid) 658 } 659 }) 660 } 661 } 662 663 var defaultRouteMatcherTests = []struct { 664 name string 665 method string 666 path string 667 valid bool 668 }{ 669 { 670 "Test route /", 671 "GET", 672 "/", 673 true, 674 }, 675 { 676 "Simple Endpoint", 677 "GET", 678 "/v1/{bucket}/do:action", 679 true, 680 }, 681 { 682 "Complex Endpoint", 683 "POST", 684 "/v1/b/{bucket_name=buckets/*}/o/{name}", 685 true, 686 }, 687 { 688 "Wildcard Endpoint", 689 "GET", 690 "/v1/endpoint/*", 691 true, 692 }, 693 { 694 "Invalid Endpoint", 695 "POST", 696 "v1/b/:name/do", 697 false, 698 }, 699 } 700 701 func TestServeMux_HandlePath(t *testing.T) { 702 mux := runtime.NewServeMux() 703 testFn := func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { 704 } 705 for _, tt := range defaultRouteMatcherTests { 706 t.Run(tt.name, func(t *testing.T) { 707 err := mux.HandlePath(tt.method, tt.path, testFn) 708 if tt.valid && err != nil { 709 t.Errorf("The route %v with method %v and path %v invalid, got %v", tt.name, tt.method, tt.path, err) 710 } 711 if !tt.valid && err == nil { 712 t.Errorf("The route %v with method %v and path %v should be invalid", tt.name, tt.method, tt.path) 713 } 714 }) 715 } 716 } 717 718 var healthCheckTests = []struct { 719 name string 720 code codes.Code 721 status grpc_health_v1.HealthCheckResponse_ServingStatus 722 httpStatusCode int 723 }{ 724 { 725 "Test grpc error code", 726 codes.NotFound, 727 grpc_health_v1.HealthCheckResponse_UNKNOWN, 728 http.StatusNotFound, 729 }, 730 { 731 "Test HealthCheckResponse_SERVING", 732 codes.OK, 733 grpc_health_v1.HealthCheckResponse_SERVING, 734 http.StatusOK, 735 }, 736 { 737 "Test HealthCheckResponse_NOT_SERVING", 738 codes.OK, 739 grpc_health_v1.HealthCheckResponse_NOT_SERVING, 740 http.StatusServiceUnavailable, 741 }, 742 { 743 "Test HealthCheckResponse_UNKNOWN", 744 codes.OK, 745 grpc_health_v1.HealthCheckResponse_UNKNOWN, 746 http.StatusServiceUnavailable, 747 }, 748 { 749 "Test HealthCheckResponse_SERVICE_UNKNOWN", 750 codes.OK, 751 grpc_health_v1.HealthCheckResponse_SERVICE_UNKNOWN, 752 http.StatusNotFound, 753 }, 754 } 755 756 func TestWithHealthzEndpoint_codes(t *testing.T) { 757 for _, tt := range healthCheckTests { 758 t.Run(tt.name, func(t *testing.T) { 759 mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(&dummyHealthCheckClient{status: tt.status, code: tt.code})) 760 761 r := httptest.NewRequest(http.MethodGet, "/healthz", nil) 762 rr := httptest.NewRecorder() 763 764 mux.ServeHTTP(rr, r) 765 766 if rr.Code != tt.httpStatusCode { 767 t.Errorf( 768 "result http status code for grpc code %q and status %q should be %d, got %d", 769 tt.code, tt.status, tt.httpStatusCode, rr.Code, 770 ) 771 } 772 }) 773 } 774 } 775 776 func TestWithHealthEndpointAt_consistentWithHealthz(t *testing.T) { 777 const endpointPath = "/healthz" 778 779 r := httptest.NewRequest(http.MethodGet, endpointPath, nil) 780 781 for _, tt := range healthCheckTests { 782 tt := tt 783 784 t.Run(tt.name, func(t *testing.T) { 785 client := &dummyHealthCheckClient{ 786 status: tt.status, 787 code: tt.code, 788 } 789 790 w := httptest.NewRecorder() 791 792 runtime.NewServeMux( 793 runtime.WithHealthEndpointAt(client, endpointPath), 794 ).ServeHTTP(w, r) 795 796 refW := httptest.NewRecorder() 797 798 runtime.NewServeMux( 799 runtime.WithHealthzEndpoint(client), 800 ).ServeHTTP(refW, r) 801 802 if w.Code != refW.Code { 803 t.Errorf( 804 "result http status code for grpc code %q and status %q should be equal to %d, but got %d", 805 tt.code, tt.status, refW.Code, w.Code, 806 ) 807 } 808 }) 809 } 810 } 811 812 func TestWithHealthzEndpoint_serviceParam(t *testing.T) { 813 service := "test" 814 815 // trigger error to output service in body 816 dummyClient := dummyHealthCheckClient{status: grpc_health_v1.HealthCheckResponse_UNKNOWN, code: codes.Unknown} 817 mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(&dummyClient)) 818 819 r := httptest.NewRequest(http.MethodGet, "/healthz?service="+service, nil) 820 rr := httptest.NewRecorder() 821 822 mux.ServeHTTP(rr, r) 823 824 if !strings.Contains(rr.Body.String(), service) { 825 t.Errorf( 826 "service query parameter should be translated to HealthCheckRequest: expected %s to contain %s", 827 rr.Body.String(), service, 828 ) 829 } 830 } 831 832 func TestWithHealthzEndpoint_header(t *testing.T) { 833 for _, tt := range healthCheckTests { 834 t.Run(tt.name, func(t *testing.T) { 835 mux := runtime.NewServeMux(runtime.WithHealthzEndpoint(&dummyHealthCheckClient{status: tt.status, code: tt.code})) 836 837 r := httptest.NewRequest(http.MethodGet, "/healthz", nil) 838 rr := httptest.NewRecorder() 839 840 mux.ServeHTTP(rr, r) 841 842 if actualHeader := rr.Header().Get("Content-Type"); actualHeader != "application/json" { 843 t.Errorf( 844 "result http header Content-Type for grpc code %q and status %q should be application/json, got %s", 845 tt.code, tt.status, actualHeader, 846 ) 847 } 848 }) 849 } 850 } 851 852 var _ grpc_health_v1.HealthClient = (*dummyHealthCheckClient)(nil) 853 854 type dummyHealthCheckClient struct { 855 status grpc_health_v1.HealthCheckResponse_ServingStatus 856 code codes.Code 857 } 858 859 func (g *dummyHealthCheckClient) Check(ctx context.Context, r *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (*grpc_health_v1.HealthCheckResponse, error) { 860 if g.code != codes.OK { 861 return nil, status.Error(g.code, r.GetService()) 862 } 863 864 return &grpc_health_v1.HealthCheckResponse{Status: g.status}, nil 865 } 866 867 func (g *dummyHealthCheckClient) Watch(ctx context.Context, r *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (grpc_health_v1.Health_WatchClient, error) { 868 return nil, status.Error(codes.Unimplemented, "unimplemented") 869 }