| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- 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())
- }
- })
- }
|