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  }