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 argument", }, { name: "unavailable is exposed as upstream error", code: Unavailable, grpcCode: codes.Unavailable, httpStatus: httpStatusBadGateway, publicCode: "UPSTREAM_ERROR", publicMessage: "upstream service error", }, { name: "ledger conflict uses aborted grpc but http conflict", code: LedgerConflict, grpcCode: codes.Aborted, httpStatus: httpStatusConflict, publicCode: string(LedgerConflict), publicMessage: "conflict", }, { name: "country cooldown keeps actionable app message", code: CountryChangeCooldown, grpcCode: codes.FailedPrecondition, httpStatus: httpStatusConflict, publicCode: string(CountryChangeCooldown), publicMessage: "country change is cooling down", }, } 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 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 }