go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/prpc/server_test.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package prpc
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"net/http"
    21  	"net/http/httptest"
    22  	"strconv"
    23  	"testing"
    24  
    25  	"google.golang.org/genproto/googleapis/rpc/errdetails"
    26  	"google.golang.org/grpc/codes"
    27  	"google.golang.org/grpc/metadata"
    28  	"google.golang.org/grpc/status"
    29  
    30  	"go.chromium.org/luci/server/router"
    31  
    32  	"github.com/golang/protobuf/proto"
    33  	. "github.com/smartystreets/goconvey/convey"
    34  )
    35  
    36  type greeterService struct {
    37  	headerMD   metadata.MD
    38  	errDetails []proto.Message
    39  }
    40  
    41  func (s *greeterService) SayHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) {
    42  	if req.Name == "" {
    43  		return nil, status.Errorf(codes.InvalidArgument, "Name unspecified")
    44  	}
    45  
    46  	if len(s.errDetails) > 0 {
    47  		st, err := status.New(codes.Unknown, "").WithDetails(s.errDetails...)
    48  		if err == nil {
    49  			err = st.Err()
    50  		}
    51  		return nil, err
    52  	}
    53  
    54  	if s.headerMD != nil {
    55  		SetHeader(ctx, s.headerMD)
    56  	}
    57  
    58  	return &HelloReply{
    59  		Message: "Hello " + req.Name,
    60  	}, nil
    61  }
    62  
    63  type calcService struct{}
    64  
    65  func (s *calcService) Multiply(ctx context.Context, req *MultiplyRequest) (*MultiplyResponse, error) {
    66  	return &MultiplyResponse{
    67  		Z: req.X & req.Y,
    68  	}, nil
    69  }
    70  
    71  func TestServer(t *testing.T) {
    72  	t.Parallel()
    73  
    74  	Convey("Greeter service", t, func() {
    75  		server := Server{}
    76  
    77  		greeterSvc := &greeterService{}
    78  		RegisterGreeterServer(&server, greeterSvc)
    79  
    80  		Convey("Register Calc service", func() {
    81  			RegisterCalcServer(&server, &calcService{})
    82  			So(server.ServiceNames(), ShouldResemble, []string{
    83  				"prpc.Calc",
    84  				"prpc.Greeter",
    85  			})
    86  		})
    87  
    88  		Convey("Handlers", func() {
    89  			c := context.Background()
    90  			r := router.New()
    91  			server.InstallHandlers(r, router.NewMiddlewareChain(
    92  				func(ctx *router.Context, next router.Handler) {
    93  					ctx.Request = ctx.Request.WithContext(c)
    94  					next(ctx)
    95  				},
    96  			))
    97  			res := httptest.NewRecorder()
    98  			hiMsg := bytes.NewBufferString(`name: "Lucy"`)
    99  			req, err := http.NewRequest("POST", "/prpc/prpc.Greeter/SayHello", hiMsg)
   100  			So(err, ShouldBeNil)
   101  			req.Header.Set("Content-Type", mtPRPCText)
   102  
   103  			invalidArgument := strconv.Itoa(int(codes.InvalidArgument))
   104  			unimplemented := strconv.Itoa(int(codes.Unimplemented))
   105  
   106  			Convey("Works", func() {
   107  				req.Header.Set("Accept", mtPRPCText)
   108  				r.ServeHTTP(res, req)
   109  				So(res.Code, ShouldEqual, http.StatusOK)
   110  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, "0")
   111  				So(res.Header().Get("X-Content-Type-Options"), ShouldEqual, "nosniff")
   112  				So(res.Body.String(), ShouldEqual, "message: \"Hello Lucy\"\n")
   113  			})
   114  
   115  			Convey("Header Metadata", func() {
   116  				greeterSvc.headerMD = metadata.Pairs("a", "1", "b", "2")
   117  				r.ServeHTTP(res, req)
   118  				So(res.Code, ShouldEqual, http.StatusOK)
   119  				So(res.Header()["A"], ShouldResemble, []string{"1"})
   120  				So(res.Header()["B"], ShouldResemble, []string{"2"})
   121  			})
   122  
   123  			Convey("Status details", func() {
   124  				greeterSvc.errDetails = []proto.Message{&errdetails.DebugInfo{Detail: "x"}}
   125  				r.ServeHTTP(res, req)
   126  				So(res.Header()[HeaderStatusDetail], ShouldResemble, []string{
   127  					"Cih0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5ycGMuRGVidWdJbmZvEgMSAXg=",
   128  				})
   129  			})
   130  
   131  			Convey("Invalid Accept header", func() {
   132  				req.Header.Set("Accept", "blah")
   133  				r.ServeHTTP(res, req)
   134  				So(res.Code, ShouldEqual, http.StatusNotAcceptable)
   135  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, invalidArgument)
   136  			})
   137  
   138  			Convey("Invalid header", func() {
   139  				req.Header.Set("X-Bin", "zzz")
   140  				r.ServeHTTP(res, req)
   141  				So(res.Code, ShouldEqual, http.StatusBadRequest)
   142  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, invalidArgument)
   143  			})
   144  
   145  			Convey("Malformed request message", func() {
   146  				hiMsg.WriteString("\nblah")
   147  				r.ServeHTTP(res, req)
   148  				So(res.Code, ShouldEqual, http.StatusBadRequest)
   149  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, invalidArgument)
   150  				So(res.Header().Get("X-Content-Type-Options"), ShouldEqual, "nosniff")
   151  			})
   152  
   153  			Convey("Invalid request message", func() {
   154  				hiMsg.Reset()
   155  				r.ServeHTTP(res, req)
   156  				So(res.Code, ShouldEqual, http.StatusBadRequest)
   157  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, invalidArgument)
   158  				So(res.Body.String(), ShouldEqual, "Name unspecified\n")
   159  			})
   160  
   161  			Convey("no such service", func() {
   162  				req.URL.Path = "/prpc/xxx/SayHello"
   163  				r.ServeHTTP(res, req)
   164  				So(res.Code, ShouldEqual, http.StatusNotImplemented)
   165  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, unimplemented)
   166  			})
   167  
   168  			Convey("no such method", func() {
   169  				req.URL.Path = "/prpc/Greeter/xxx"
   170  				r.ServeHTTP(res, req)
   171  				So(res.Code, ShouldEqual, http.StatusNotImplemented)
   172  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, unimplemented)
   173  			})
   174  
   175  			Convey(`When access control is enabled without credentials`, func() {
   176  				server.AccessControl = func(ctx context.Context, origin string) AccessControlDecision {
   177  					return AccessControlDecision{
   178  						AllowCrossOriginRequests: true,
   179  						AllowCredentials:         false,
   180  					}
   181  				}
   182  				req.Header.Add("Origin", "http://example.com")
   183  
   184  				r.ServeHTTP(res, req)
   185  				So(res.Code, ShouldEqual, http.StatusOK)
   186  				So(res.Header().Get(HeaderGRPCCode), ShouldEqual, "0")
   187  				So(res.Header().Get("Access-Control-Allow-Origin"), ShouldEqual, "http://example.com")
   188  				So(res.Header().Get("Access-Control-Allow-Credentials"), ShouldEqual, "")
   189  				So(res.Header().Get("Access-Control-Expose-Headers"), ShouldEqual, HeaderGRPCCode)
   190  			})
   191  
   192  			Convey(`When access control is enabled for "http://example.com"`, func() {
   193  				decision := AccessControlDecision{
   194  					AllowCrossOriginRequests: true,
   195  					AllowCredentials:         true,
   196  				}
   197  				server.AccessControl = func(ctx context.Context, origin string) AccessControlDecision {
   198  					if origin == "http://example.com" {
   199  						return decision
   200  					}
   201  					return AccessControlDecision{}
   202  				}
   203  
   204  				Convey(`When sending an OPTIONS request`, func() {
   205  					req.Method = "OPTIONS"
   206  
   207  					Convey(`Will supply Access-* headers to "http://example.com"`, func() {
   208  						req.Header.Add("Origin", "http://example.com")
   209  
   210  						r.ServeHTTP(res, req)
   211  						So(res.Code, ShouldEqual, http.StatusOK)
   212  						So(res.Header().Get("Access-Control-Allow-Origin"), ShouldEqual, "http://example.com")
   213  						So(res.Header().Get("Access-Control-Allow-Credentials"), ShouldEqual, "true")
   214  						So(res.Header().Get("Access-Control-Allow-Headers"), ShouldEqual, "Origin, Content-Type, Accept, Authorization")
   215  						So(res.Header().Get("Access-Control-Allow-Methods"), ShouldEqual, "OPTIONS, POST")
   216  						So(res.Header().Get("Access-Control-Max-Age"), ShouldEqual, "600")
   217  					})
   218  
   219  					Convey(`Will not supply access-* headers to "http://foo.bar"`, func() {
   220  						req.Header.Add("Origin", "http://foo.bar")
   221  
   222  						r.ServeHTTP(res, req)
   223  						So(res.Code, ShouldEqual, http.StatusOK)
   224  						So(res.Header().Get("Access-Control-Allow-Origin"), ShouldEqual, "")
   225  					})
   226  				})
   227  
   228  				Convey(`When sending a POST request`, func() {
   229  					Convey(`Will supply Access-* headers to "http://example.com"`, func() {
   230  						req.Header.Add("Origin", "http://example.com")
   231  
   232  						r.ServeHTTP(res, req)
   233  						So(res.Code, ShouldEqual, http.StatusOK)
   234  						So(res.Header().Get(HeaderGRPCCode), ShouldEqual, "0")
   235  						So(res.Header().Get("Access-Control-Allow-Origin"), ShouldEqual, "http://example.com")
   236  						So(res.Header().Get("Access-Control-Allow-Credentials"), ShouldEqual, "true")
   237  						So(res.Header().Get("Access-Control-Expose-Headers"), ShouldEqual, HeaderGRPCCode)
   238  					})
   239  
   240  					Convey(`Will not supply access-* headers to "http://foo.bar"`, func() {
   241  						req.Header.Add("Origin", "http://foo.bar")
   242  
   243  						r.ServeHTTP(res, req)
   244  						So(res.Code, ShouldEqual, http.StatusOK)
   245  						So(res.Header().Get(HeaderGRPCCode), ShouldEqual, "0")
   246  						So(res.Header().Get("Access-Control-Allow-Origin"), ShouldEqual, "")
   247  					})
   248  				})
   249  
   250  				Convey(`Using custom AllowHeaders`, func() {
   251  					decision.AllowHeaders = []string{"Booboo", "bobo"}
   252  
   253  					req.Method = "OPTIONS"
   254  					req.Header.Add("Origin", "http://example.com")
   255  
   256  					r.ServeHTTP(res, req)
   257  					So(res.Code, ShouldEqual, http.StatusOK)
   258  					So(res.Header().Get("Access-Control-Allow-Origin"), ShouldEqual, "http://example.com")
   259  					So(res.Header().Get("Access-Control-Allow-Credentials"), ShouldEqual, "true")
   260  					So(res.Header().Get("Access-Control-Allow-Headers"), ShouldEqual, "Booboo, bobo, Origin, Content-Type, Accept, Authorization")
   261  					So(res.Header().Get("Access-Control-Allow-Methods"), ShouldEqual, "OPTIONS, POST")
   262  					So(res.Header().Get("Access-Control-Max-Age"), ShouldEqual, "600")
   263  				})
   264  			})
   265  		})
   266  	})
   267  }