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: "cp conflict identifies current user", code: CPAlreadyExistsSelf, grpcCode: codes.FailedPrecondition, httpStatus: httpStatusConflict, publicCode: string(CPAlreadyExistsSelf), publicMessage: "You already have a CP", }, { name: "cp conflict identifies other party", code: CPAlreadyExistsOther, grpcCode: codes.FailedPrecondition, httpStatus: httpStatusConflict, publicCode: string(CPAlreadyExistsOther), publicMessage: "The other party already has a CP", }, { 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 }