package rulecaps import ( "encoding/json" "errors" "strings" "testing" "github.com/perfect-panel/server/internal/model/lottery" ) func TestValidateEligibilityJSON_AcceptsEmpty(t *testing.T) { cases := [][]byte{ nil, []byte(""), []byte("{}"), []byte("null"), []byte(" \n \t "), } for _, c := range cases { if err := ValidateEligibilityJSON(c); err != nil { t.Fatalf("expected accept for %q, got %v", string(c), err) } } } func TestValidateEligibilityJSON_RejectsOversizePayload(t *testing.T) { blob := make([]byte, MaxBytes+1) for i := range blob { blob[i] = 'a' } err := ValidateEligibilityJSON(blob) if !errors.Is(err, ErrRuleTreeTooLarge) { t.Fatalf("expected ErrRuleTreeTooLarge, got %v", err) } // user-facing message should name the limit if !strings.Contains(err.Error(), "8192") { t.Fatalf("expected message to mention 8192 byte limit, got %q", err.Error()) } } func TestValidateEligibilityJSON_RejectsBadJSON(t *testing.T) { err := ValidateEligibilityJSON([]byte(`{bad`)) if err == nil { t.Fatalf("expected error on invalid JSON") } } // buildDeepTree 构造 depth 层单链嵌套(每层一个 OR 聚合)。root 为第 1 层。 func buildDeepTree(depth int) *lottery.EligibilityRule { root := &lottery.EligibilityRule{Op: "OR", Children: []*lottery.EligibilityRule{{Type: "has_subscription"}}} current := root for i := 2; i < depth; i++ { next := &lottery.EligibilityRule{Op: "OR", Children: []*lottery.EligibilityRule{{Type: "has_subscription"}}} current.Children = []*lottery.EligibilityRule{next} current = next } return root } func TestValidateEligibilityJSON_RejectsDepthOverLimit(t *testing.T) { tree := buildDeepTree(MaxDepth + 1) // depth 9 with defaults raw, err := json.Marshal(tree) if err != nil { t.Fatalf("marshal: %v", err) } err = ValidateEligibilityJSON(raw) if !errors.Is(err, ErrRuleTreeTooDeep) { t.Fatalf("expected ErrRuleTreeTooDeep, got %v", err) } } func TestValidateEligibilityJSON_AcceptsMaxDepth(t *testing.T) { tree := buildDeepTree(MaxDepth) raw, _ := json.Marshal(tree) if err := ValidateEligibilityJSON(raw); err != nil { t.Fatalf("depth=MaxDepth must be accepted, got %v", err) } } // buildWideTree 构造根节点 + N 个叶子,总节点数 = 1 + N。 func buildWideTree(leaves int) *lottery.EligibilityRule { root := &lottery.EligibilityRule{Op: "AND"} for i := 0; i < leaves; i++ { root.Children = append(root.Children, &lottery.EligibilityRule{Type: "has_subscription"}) } return root } func TestValidateEligibilityJSON_RejectsNodeCountOverLimit(t *testing.T) { // 65 nodes total = 1 root + 64 leaves > MaxNodes tree := buildWideTree(MaxNodes) raw, _ := json.Marshal(tree) err := ValidateEligibilityJSON(raw) if !errors.Is(err, ErrRuleTreeTooManyNodes) { t.Fatalf("expected ErrRuleTreeTooManyNodes, got %v", err) } } func TestValidateEligibilityJSON_AcceptsAtNodeLimit(t *testing.T) { // 64 nodes = 1 root + 63 leaves tree := buildWideTree(MaxNodes - 1) raw, _ := json.Marshal(tree) if err := ValidateEligibilityJSON(raw); err != nil { t.Fatalf("nodes=MaxNodes must be accepted, got %v", err) } } func TestValidateEligibilityJSON_AcceptsRealisticTree(t *testing.T) { // Typical activity: (has_subscription AND invite_count>=3) OR user_tag in {vip} raw := []byte(`{ "op": "OR", "children": [ { "op": "AND", "children": [ {"type": "has_subscription", "params": {"min_days_remaining": 7}}, {"type": "invite_count", "params": {"min": 3}} ] }, {"type": "user_tag", "params": {"tags": ["vip"]}} ] }`) if err := ValidateEligibilityJSON(raw); err != nil { t.Fatalf("realistic tree should be accepted, got %v", err) } }