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  }