go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/testing/assertions/grpc.go (about) 1 // Copyright 2015 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 assertions 16 17 import ( 18 "fmt" 19 20 "github.com/golang/protobuf/proto" 21 "github.com/smarty/assertions" 22 . "github.com/smartystreets/goconvey/convey" 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/status" 25 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/grpc/appstatus" 28 "go.chromium.org/luci/grpc/grpcutil" 29 ) 30 31 // ShouldHaveAppStatus asserts that error `actual` has an 32 // application-specific status and it matches the expectations. 33 // See ShouldBeLikeStatus for the format of `expected`. 34 // See appstatus package for application-specific statuses. 35 func ShouldHaveAppStatus(actual any, expected ...any) string { 36 if ret := assertions.ShouldImplement(actual, (*error)(nil)); ret != "" { 37 return ret 38 } 39 actualStatus, ok := appstatus.Get(actual.(error)) 40 if !ok { 41 return fmt.Sprintf("expected error %q to have an explicit application status", actual) 42 } 43 44 return ShouldBeLikeStatus(actualStatus, expected...) 45 } 46 47 // ShouldHaveRPCCode is a goconvey assertion, asserting that the supplied 48 // "actual" value has a gRPC code value and, optionally, errors like a supplied 49 // message string. 50 // 51 // If no "expected" arguments are supplied, ShouldHaveRPCCode will assert that 52 // the result is codes.OK. 53 // 54 // The first "expected" argument, if supplied, is the gRPC codes.Code to assert. 55 // 56 // A second "expected" string may be optionally included. If included, the 57 // gRPC error message is asserted to contain the expected string using 58 // convey.ShouldContainSubstring. 59 func ShouldHaveRPCCode(actual any, expected ...any) string { 60 aerr, ok := actual.(error) 61 if !(ok || actual == nil) { 62 return "actual argument must be an error." 63 } 64 65 var ( 66 ecode codes.Code 67 errLike string 68 ) 69 switch len(expected) { 70 case 2: 71 var ok bool 72 if errLike, ok = expected[1].(string); !ok { 73 return fmt.Sprintf("The expected error substring must be a string, not a %T", expected[1]) 74 } 75 fallthrough 76 77 case 1: 78 var ok bool 79 if ecode, ok = expected[0].(codes.Code); !ok { 80 return fmt.Sprintf("The code must be a codes.Code, not a %T", expected[0]) 81 } 82 83 case 0: 84 ecode = codes.OK 85 86 default: 87 return "Expected argument must have the form: [codes.Code[string]]" 88 } 89 90 if acode := grpcutil.Code(aerr); acode != ecode { 91 return fmt.Sprintf("expected gRPC code %q (%d), not %q (%d), type %T: %v", 92 ecode, ecode, acode, acode, actual, actual) 93 } 94 95 if errLike != "" { 96 return ShouldContainSubstring(status.Convert(aerr).Message(), errLike) 97 } 98 return "" 99 } 100 101 // ShouldBeRPCOK asserts that "actual" is an error that has a gRPC code value 102 // of codes.OK. 103 // 104 // Note that "nil" has an codes.OK value. 105 // 106 // One additional "expected" string may be optionally included. If included, the 107 // gRPC error's message is asserted to contain the expected string. 108 func ShouldBeRPCOK(actual any, expected ...any) string { 109 return ShouldHaveRPCCode(actual, prepend(codes.OK, expected)...) 110 } 111 112 // ShouldBeRPCInvalidArgument asserts that "actual" is an error that has a gRPC 113 // code value of codes.InvalidArgument. 114 // 115 // One additional "expected" string may be optionally included. If included, the 116 // gRPC error's message is asserted to contain the expected string. 117 func ShouldBeRPCInvalidArgument(actual any, expected ...any) string { 118 return ShouldHaveRPCCode(actual, prepend(codes.InvalidArgument, expected)...) 119 } 120 121 // ShouldBeRPCInternal asserts that "actual" is an error that has a gRPC code 122 // value of codes.Internal. 123 // 124 // One additional "expected" string may be optionally included. If included, the 125 // gRPC error's message is asserted to contain the expected string. 126 func ShouldBeRPCInternal(actual any, expected ...any) string { 127 return ShouldHaveRPCCode(actual, prepend(codes.Internal, expected)...) 128 } 129 130 // ShouldBeRPCUnknown asserts that "actual" is an error that has a gRPC code 131 // value of codes.Unknown. 132 // 133 // One additional "expected" string may be optionally included. If included, the 134 // gRPC error's message is asserted to contain the expected string. 135 func ShouldBeRPCUnknown(actual any, expected ...any) string { 136 return ShouldHaveRPCCode(actual, prepend(codes.Unknown, expected)...) 137 } 138 139 // ShouldBeRPCNotFound asserts that "actual" is an error that has a gRPC code 140 // value of codes.NotFound. 141 // 142 // One additional "expected" string may be optionally included. If included, the 143 // gRPC error's message is asserted to contain the expected string. 144 func ShouldBeRPCNotFound(actual any, expected ...any) string { 145 return ShouldHaveRPCCode(actual, prepend(codes.NotFound, expected)...) 146 } 147 148 // ShouldBeRPCPermissionDenied asserts that "actual" is an error that has a gRPC 149 // code value of codes.PermissionDenied. 150 // 151 // One additional "expected" string may be optionally included. If included, the 152 // gRPC error's message is asserted to contain the expected string. 153 func ShouldBeRPCPermissionDenied(actual any, expected ...any) string { 154 return ShouldHaveRPCCode(actual, prepend(codes.PermissionDenied, expected)...) 155 } 156 157 // ShouldBeRPCAlreadyExists asserts that "actual" is an error that has a gRPC 158 // code value of codes.AlreadyExists. 159 // 160 // One additional "expected" string may be optionally included. If included, the 161 // gRPC error's message is asserted to contain the expected string. 162 func ShouldBeRPCAlreadyExists(actual any, expected ...any) string { 163 return ShouldHaveRPCCode(actual, prepend(codes.AlreadyExists, expected)...) 164 } 165 166 // ShouldBeRPCUnauthenticated asserts that "actual" is an error that has a gRPC 167 // code value of codes.Unauthenticated. 168 // 169 // One additional "expected" string may be optionally included. If included, the 170 // gRPC error's message is asserted to contain the expected string. 171 func ShouldBeRPCUnauthenticated(actual any, expected ...any) string { 172 return ShouldHaveRPCCode(actual, prepend(codes.Unauthenticated, expected)...) 173 } 174 175 // ShouldBeRPCFailedPrecondition asserts that "actual" is an error that has a gRPC 176 // code value of codes.FailedPrecondition. 177 // 178 // One additional "expected" string may be optionally included. If included, the 179 // gRPC error's message is asserted to contain the expected string. 180 func ShouldBeRPCFailedPrecondition(actual any, expected ...any) string { 181 return ShouldHaveRPCCode(actual, prepend(codes.FailedPrecondition, expected)...) 182 } 183 184 // ShouldBeRPCAborted asserts that "actual" is an error that has a gRPC 185 // code value of codes.Aborted. 186 // 187 // One additional "expected" string may be optionally included. If included, the 188 // gRPC error's message is asserted to contain the expected string. 189 func ShouldBeRPCAborted(actual any, expected ...any) string { 190 return ShouldHaveRPCCode(actual, prepend(codes.Aborted, expected)...) 191 } 192 193 // ShouldBeRPCDeadlineExceeded asserts that "actual" is an error that has a gRPC 194 // code value of codes.DeadlineExceeded. 195 // 196 // One additional "expected" string may be optionally included. If included, the 197 // gRPC error's message is asserted to contain the expected string. 198 func ShouldBeRPCDeadlineExceeded(actual any, expected ...any) string { 199 return ShouldHaveRPCCode(actual, prepend(codes.DeadlineExceeded, expected)...) 200 } 201 202 func prepend(c codes.Code, exp []any) []any { 203 args := make([]any, len(exp)+1) 204 args[0] = c 205 copy(args[1:], exp) 206 return args 207 } 208 209 // ShouldBeLikeStatus asserts that *status.Status `actual` has code 210 // `expected[0]`, that the actual message has a substring `expected[1]` and 211 // that the status details in expected[2:] as present in the actual status. 212 // 213 // len(expected) must be at least 1. 214 // 215 // Example: 216 // 217 // // err must have a NotFound status 218 // So(s, ShouldBeLikeStatus, codes.NotFound) 219 // 220 // // and its message must contain "item not found" 221 // So(s, ShouldBeLikeStatus, codes.NotFound, "item not found") 222 // 223 // // and it must have a DebugInfo detail. 224 // So(s, ShouldBeLikeStatus, codes.NotFound, "item not found", &errdetails.DebugInfo{Details: "x"}) 225 func ShouldBeLikeStatus(actual any, expected ...any) string { 226 if ret := assertions.ShouldHaveSameTypeAs(actual, (*status.Status)(nil)); ret != "" { 227 return ret 228 } 229 230 if ret := assertions.ShouldNotBeEmpty(expected); ret != "" { 231 return ret 232 } 233 234 actualStatus := actual.(*status.Status) 235 236 if ret := assertions.ShouldEqual(actualStatus.Code(), expected[0]); ret != "" { 237 return ret 238 } 239 240 if len(expected) == 1 { 241 return "" 242 } 243 244 if ret := assertions.ShouldContainSubstring(actualStatus.Message(), expected[1]); ret != "" { 245 return ret 246 } 247 248 if len(expected) == 2 { 249 return "" 250 } 251 252 // Serialize actual details to strings as compact text proto. 253 actualDetails := actualStatus.Details() 254 presentDetails := stringset.New(len(actualDetails)) 255 for _, d := range actualDetails { 256 presentDetails.Add(proto.CompactTextString(d.(proto.Message))) 257 } 258 259 // Then assert presence of each expected detail. 260 for _, d := range expected[2:] { 261 if ret := assertions.ShouldImplement(d, (*proto.Message)(nil)); ret != "" { 262 return ret 263 } 264 eTxt := proto.CompactTextString(d.(proto.Message)) 265 if !presentDetails.Has(eTxt) { 266 return fmt.Sprintf("expected presence of status detail %q, got %q", eTxt, presentDetails.ToSlice()) 267 } 268 } 269 270 return "" 271 } 272 273 // ShouldHaveGRPCStatus asserts that error `actual` has a GRPC status and it 274 // matches the expectations. 275 // See ShouldBeStatusLike for the format of `expected`. 276 // The status is extracted using status.FromError. 277 func ShouldHaveGRPCStatus(actual any, expected ...any) string { 278 if ret := assertions.ShouldImplement(actual, (*error)(nil)); ret != "" { 279 return ret 280 } 281 actualStatus, ok := status.FromError(actual.(error)) 282 if !ok { 283 return fmt.Sprintf("expected error %q to have a GRPC status", actual) 284 } 285 286 return ShouldBeLikeStatus(actualStatus, expected...) 287 }