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 }