package server import ( "context" "testing" "perms-system-server/internal/svc" "perms-system-server/internal/testutil" "perms-system-server/pb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // TC-0794: gRPC VerifyToken 契约层 fuzz —— 任意畸形 AccessToken 必须: // (1) 返回 (resp, nil) 而非 panic / (nil, err), 因为签名/过期/payload 问题在对外契约里都只是"无效令牌" // (2) 当返回时 resp.Valid 必须为 false (不能误把 nil token 判成有效) // // 种子覆盖常见的攻击 payload: 空串、非 JWT 结构、alg=none 试探、超长串、Unicode 噪声、控制字符。 // 在 CI 里 `go test -run ^FuzzVerifyToken$` 只会执行种子语料, 不会触发随机变异, 因此确定性高、耗时可控。 // 本地做 `go test -fuzz=FuzzVerifyToken -fuzztime=30s` 可进一步跑随机变异。 func FuzzVerifyToken_NeverPanicsAlwaysInvalid(f *testing.F) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) srv := NewPermServer(svcCtx) seeds := []string{ "", " ", ".", "..", "not.a.jwt", "a.b.c", "eyJhbGciOiJub25lIn0.eyJ1c2VySWQiOjF9.", // alg=none 试探 "Bearer xxx", "null", "\x00\x01\x02", "🔥token💥", string(make([]byte, 4096)), // 长令牌 "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjF9.sig", // 伪造 HS256 } for _, s := range seeds { f.Add(s) } f.Fuzz(func(t *testing.T, raw string) { defer func() { if r := recover(); r != nil { t.Fatalf("VerifyToken panicked on input=%q: %v", raw, r) } }() resp, err := srv.VerifyToken(context.Background(), &pb.VerifyTokenReq{AccessToken: raw}) if err != nil { t.Fatalf("VerifyToken must never return an error for malformed input, got err=%v (input=%q)", err, raw) } if resp == nil { t.Fatalf("VerifyToken must return non-nil response (input=%q)", raw) } if resp.Valid { t.Fatalf("malformed/invalid token must never be reported valid; input=%q", raw) } }) } // TC-0795: gRPC GetUserPerms 契约层 fuzz —— 任意 (appKey, appSecret, productCode, userId) 组合下: // (1) 必须返回 status.Error(非 200); 不允许 panic / nil error + 有权限返回 // (2) 错误码必须落在固定集合内: Unauthenticated / PermissionDenied / InvalidArgument / NotFound / Internal // —— 否则契约漂移, 产品侧"权限网关"无法稳定处理 // // 此用例不需要预置任何数据, 专打输入校验/认证失败的快速拒绝路径。 func FuzzGetUserPerms_ErrorTaxonomyStable(f *testing.F) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) srv := NewPermServer(svcCtx) seeds := [][4]string{ {"", "", "", ""}, {"nonexistent_appkey_" + testutil.UniqueId(), "x", "p", "1"}, {"appkey", "wrong_secret", "code", "0"}, {"🔑", "🔒", "😈", "-1"}, {"'; DROP TABLE sys_product; --", "s", "p", "1"}, {string(make([]byte, 512)), "s", "p", "1"}, } for _, s := range seeds { f.Add(s[0], s[1], s[2], s[3]) } allowed := map[codes.Code]bool{ codes.Unauthenticated: true, codes.PermissionDenied: true, codes.InvalidArgument: true, codes.NotFound: true, codes.Internal: true, } f.Fuzz(func(t *testing.T, appKey, appSecret, productCode, userIdStr string) { defer func() { if r := recover(); r != nil { t.Fatalf("GetUserPerms panicked on input=(%q,%q,%q,%q): %v", appKey, appSecret, productCode, userIdStr, r) } }() var uid int64 for _, c := range userIdStr { if c >= '0' && c <= '9' { uid = uid*10 + int64(c-'0') if uid > 1e15 { break } } } _, err := srv.GetUserPerms(context.Background(), &pb.GetUserPermsReq{ AppKey: appKey, AppSecret: appSecret, ProductCode: productCode, UserId: uid, }) if err == nil { t.Fatalf("malformed/unauthenticated input must produce an error; appKey=%q", appKey) } st, ok := status.FromError(err) if !ok { t.Fatalf("error must be a grpc status.Error, got %T (%v)", err, err) } if !allowed[st.Code()] { t.Fatalf("error code %s is outside the agreed contract taxonomy; must be one of Unauthenticated/PermissionDenied/InvalidArgument/NotFound/Internal. msg=%q", st.Code(), st.Message()) } }) }