blob: ff85f93f9d6e454b85f1d4d3cb3b34174a2a9de7 [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assertions
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/smarty/assertions"
"github.com/smartystreets/goconvey/convey"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/grpc/appstatus"
"go.chromium.org/luci/grpc/grpcutil"
)
// ShouldHaveAppStatus asserts that error `actual` has an
// application-specific status and it matches the expectations.
// See ShouldBeLikeStatus for the format of `expected`.
// See appstatus package for application-specific statuses.
func ShouldHaveAppStatus(actual any, expected ...any) string {
if ret := assertions.ShouldImplement(actual, (*error)(nil)); ret != "" {
return ret
}
actualStatus, ok := appstatus.Get(actual.(error))
if !ok {
return fmt.Sprintf("expected error %q to have an explicit application status", actual)
}
return ShouldBeLikeStatus(actualStatus, expected...)
}
// ShouldHaveRPCCode is a goconvey assertion, asserting that the supplied
// "actual" value has a gRPC code value and, optionally, errors like a supplied
// message string.
//
// If no "expected" arguments are supplied, ShouldHaveRPCCode will assert that
// the result is codes.OK.
//
// The first "expected" argument, if supplied, is the gRPC codes.Code to assert.
//
// A second "expected" string may be optionally included. If included, the
// gRPC error message is asserted to contain the expected string using
// convey.ShouldContainSubstring.
func ShouldHaveRPCCode(actual any, expected ...any) string {
aerr, ok := actual.(error)
if !(ok || actual == nil) {
return "actual argument must be an error."
}
var (
ecode codes.Code
errLike string
)
switch len(expected) {
case 2:
var ok bool
if errLike, ok = expected[1].(string); !ok {
return fmt.Sprintf("The expected error substring must be a string, not a %T", expected[1])
}
fallthrough
case 1:
var ok bool
if ecode, ok = expected[0].(codes.Code); !ok {
return fmt.Sprintf("The code must be a codes.Code, not a %T", expected[0])
}
case 0:
ecode = codes.OK
default:
return "Expected argument must have the form: [codes.Code[string]]"
}
if acode := grpcutil.Code(aerr); acode != ecode {
return fmt.Sprintf("expected gRPC code %q (%d), not %q (%d), type %T: %v",
ecode, ecode, acode, acode, actual, actual)
}
if errLike != "" {
return convey.ShouldContainSubstring(status.Convert(aerr).Message(), errLike)
}
return ""
}
// ShouldBeRPCOK asserts that "actual" is an error that has a gRPC code value
// of codes.OK.
//
// Note that "nil" has an codes.OK value.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCOK(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.OK, expected)...)
}
// ShouldBeRPCInvalidArgument asserts that "actual" is an error that has a gRPC
// code value of codes.InvalidArgument.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCInvalidArgument(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.InvalidArgument, expected)...)
}
// ShouldBeRPCInternal asserts that "actual" is an error that has a gRPC code
// value of codes.Internal.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCInternal(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.Internal, expected)...)
}
// ShouldBeRPCUnknown asserts that "actual" is an error that has a gRPC code
// value of codes.Unknown.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCUnknown(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.Unknown, expected)...)
}
// ShouldBeRPCNotFound asserts that "actual" is an error that has a gRPC code
// value of codes.NotFound.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCNotFound(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.NotFound, expected)...)
}
// ShouldBeRPCPermissionDenied asserts that "actual" is an error that has a gRPC
// code value of codes.PermissionDenied.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCPermissionDenied(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.PermissionDenied, expected)...)
}
// ShouldBeRPCAlreadyExists asserts that "actual" is an error that has a gRPC
// code value of codes.AlreadyExists.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCAlreadyExists(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.AlreadyExists, expected)...)
}
// ShouldBeRPCUnauthenticated asserts that "actual" is an error that has a gRPC
// code value of codes.Unauthenticated.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCUnauthenticated(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.Unauthenticated, expected)...)
}
// ShouldBeRPCFailedPrecondition asserts that "actual" is an error that has a gRPC
// code value of codes.FailedPrecondition.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCFailedPrecondition(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.FailedPrecondition, expected)...)
}
// ShouldBeRPCAborted asserts that "actual" is an error that has a gRPC
// code value of codes.Aborted.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCAborted(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.Aborted, expected)...)
}
// ShouldBeRPCDeadlineExceeded asserts that "actual" is an error that has a gRPC
// code value of codes.DeadlineExceeded.
//
// One additional "expected" string may be optionally included. If included, the
// gRPC error's message is asserted to contain the expected string.
func ShouldBeRPCDeadlineExceeded(actual any, expected ...any) string {
return ShouldHaveRPCCode(actual, prepend(codes.DeadlineExceeded, expected)...)
}
func prepend(c codes.Code, exp []any) []any {
args := make([]any, len(exp)+1)
args[0] = c
copy(args[1:], exp)
return args
}
// ShouldBeLikeStatus asserts that *status.Status `actual` has code
// `expected[0]`, that the actual message has a substring `expected[1]` and
// that the status details in expected[2:] as present in the actual status.
//
// len(expected) must be at least 1.
//
// Example:
//
// // err must have a NotFound status
// So(s, ShouldBeLikeStatus, codes.NotFound)
//
// // and its message must contain "item not found"
// So(s, ShouldBeLikeStatus, codes.NotFound, "item not found")
//
// // and it must have a DebugInfo detail.
// So(s, ShouldBeLikeStatus, codes.NotFound, "item not found", &errdetails.DebugInfo{Details: "x"})
func ShouldBeLikeStatus(actual any, expected ...any) string {
if ret := assertions.ShouldHaveSameTypeAs(actual, (*status.Status)(nil)); ret != "" {
return ret
}
if ret := assertions.ShouldNotBeEmpty(expected); ret != "" {
return ret
}
actualStatus := actual.(*status.Status)
if ret := assertions.ShouldEqual(actualStatus.Code(), expected[0]); ret != "" {
return ret
}
if len(expected) == 1 {
return ""
}
if ret := assertions.ShouldContainSubstring(actualStatus.Message(), expected[1]); ret != "" {
return ret
}
if len(expected) == 2 {
return ""
}
// Serialize actual details to strings as compact text proto.
actualDetails := actualStatus.Details()
presentDetails := stringset.New(len(actualDetails))
for _, d := range actualDetails {
presentDetails.Add(proto.CompactTextString(d.(proto.Message)))
}
// Then assert presence of each expected detail.
for _, d := range expected[2:] {
if ret := assertions.ShouldImplement(d, (*proto.Message)(nil)); ret != "" {
return ret
}
eTxt := proto.CompactTextString(d.(proto.Message))
if !presentDetails.Has(eTxt) {
return fmt.Sprintf("expected presence of status detail %q, got %q", eTxt, presentDetails.ToSlice())
}
}
return ""
}
// ShouldHaveGRPCStatus asserts that error `actual` has a GRPC status and it
// matches the expectations.
// See ShouldBeStatusLike for the format of `expected`.
// The status is extracted using status.FromError.
func ShouldHaveGRPCStatus(actual any, expected ...any) string {
if ret := assertions.ShouldImplement(actual, (*error)(nil)); ret != "" {
return ret
}
actualStatus, ok := status.FromError(actual.(error))
if !ok {
return fmt.Sprintf("expected error %q to have a GRPC status", actual)
}
return ShouldBeLikeStatus(actualStatus, expected...)
}