hyapp-server/pkg/xerr/grpc_test.go

170 lines
4.4 KiB
Go

package xerr
import (
"go/ast"
"go/parser"
"go/token"
"strconv"
"testing"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// TestToGRPCErrorCarriesReason 锁定服务底座要求的 gRPC code + ErrorInfo.reason 组合。
func TestToGRPCErrorCarriesReason(t *testing.T) {
err := ToGRPCError(New(PasswordAlreadySet, "password already set"))
st, ok := status.FromError(err)
if !ok {
t.Fatalf("expected grpc status")
}
if st.Code() != codes.AlreadyExists {
t.Fatalf("grpc code mismatch: got %s", st.Code())
}
if got := ReasonFromGRPC(err); got != PasswordAlreadySet {
t.Fatalf("reason mismatch: got %s want %s", got, PasswordAlreadySet)
}
}
func TestCatalogCoversDeclaredCodes(t *testing.T) {
declared := declaredCodesFromSource(t)
for code, name := range declared {
if _, ok := catalog[code]; !ok {
t.Fatalf("declared xerr code %s (%s) is missing from catalog", name, code)
}
}
for code := range catalog {
if _, ok := declared[code]; !ok {
t.Fatalf("catalog registers undeclared xerr code %s", code)
}
}
}
func TestCatalogMappings(t *testing.T) {
tests := []struct {
name string
code Code
grpcCode codes.Code
httpStatus int
publicCode string
publicMessage string
}{
{
name: "business reason keeps canonical grpc code",
code: InvalidInviteCode,
grpcCode: codes.InvalidArgument,
httpStatus: httpStatusBadRequest,
publicCode: string(InvalidInviteCode),
publicMessage: "invalid invite code",
},
{
name: "unavailable is exposed as upstream error",
code: Unavailable,
grpcCode: codes.Unavailable,
httpStatus: httpStatusBadGateway,
publicCode: "UPSTREAM_ERROR",
publicMessage: "upstream service error",
},
{
name: "ledger state change uses aborted grpc but actionable http message",
code: LedgerConflict,
grpcCode: codes.Aborted,
httpStatus: httpStatusConflict,
publicCode: string(LedgerConflict),
publicMessage: "wallet state changed, please retry",
},
{
name: "country cooldown keeps actionable app message",
code: CountryChangeCooldown,
grpcCode: codes.FailedPrecondition,
httpStatus: httpStatusConflict,
publicCode: string(CountryChangeCooldown),
publicMessage: "country change is cooling down",
},
{
name: "registered device conflict exposes concrete login reason",
code: DeviceAlreadyRegistered,
grpcCode: codes.FailedPrecondition,
httpStatus: httpStatusConflict,
publicCode: string(DeviceAlreadyRegistered),
publicMessage: "device already registered",
},
{
name: "region mismatch keeps forbidden transport but exposes concrete app prompt",
code: RegionMismatch,
grpcCode: codes.PermissionDenied,
httpStatus: httpStatusForbidden,
publicCode: string(RegionMismatch),
publicMessage: "不是同一个地区",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
spec := SpecOf(test.code)
if spec.GRPCCode != test.grpcCode ||
spec.HTTPStatus != test.httpStatus ||
spec.PublicCode != test.publicCode ||
spec.PublicMessage != test.publicMessage {
t.Fatalf("catalog mismatch: got %+v", spec)
}
})
}
}
func TestCatalogDoesNotExposeBareConflictMessage(t *testing.T) {
for _, code := range RegisteredCodes() {
spec := SpecOf(code)
if spec.PublicMessage == "conflict" {
t.Fatalf("code %s exposes bare conflict message", code)
}
}
}
func declaredCodesFromSource(t *testing.T) map[Code]string {
t.Helper()
file, err := parser.ParseFile(token.NewFileSet(), "errors.go", nil, 0)
if err != nil {
t.Fatalf("parse errors.go: %v", err)
}
declared := make(map[Code]string)
ast.Inspect(file, func(node ast.Node) bool {
decl, ok := node.(*ast.GenDecl)
if !ok || decl.Tok != token.CONST {
return true
}
for _, spec := range decl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for index, name := range valueSpec.Names {
if index >= len(valueSpec.Values) {
continue
}
literal, ok := valueSpec.Values[index].(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
continue
}
value, err := strconv.Unquote(literal.Value)
if err != nil {
t.Fatalf("unquote %s: %v", literal.Value, err)
}
declared[Code(value)] = name.Name
}
}
return false
})
return declared
}