hyapp-server/pkg/xerr/grpc_test.go

195 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package xerr
import (
"context"
"fmt"
"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)
}
}
// TestToGRPCErrorPreservesContextTermination 防止并发聚合器主动取消 sibling RPC 时,
// database/sql 返回的 context 错误被兜底成 Internal污染告警并掩盖真正的首个失败。
func TestToGRPCErrorPreservesContextTermination(t *testing.T) {
tests := []struct {
name string
err error
code codes.Code
}{
{name: "canceled", err: context.Canceled, code: codes.Canceled},
{name: "wrapped canceled", err: fmt.Errorf("query interrupted: %w", context.Canceled), code: codes.Canceled},
{name: "deadline exceeded", err: context.DeadlineExceeded, code: codes.DeadlineExceeded},
{name: "wrapped deadline exceeded", err: fmt.Errorf("query interrupted: %w", context.DeadlineExceeded), code: codes.DeadlineExceeded},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := status.Code(ToGRPCError(test.err)); got != test.code {
t.Fatalf("grpc code mismatch: got %s want %s", got, test.code)
}
})
}
}
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
}