170 lines
4.4 KiB
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
|
|
}
|