package lottery import ( "context" "encoding/json" "testing" ) func mustParams(t *testing.T, v any) json.RawMessage { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal params: %v", err) } return b } func TestEvaluator_HasSubscription(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{ Type: RuleTypeHasSubscription, Params: mustParams(t, map[string]int64{"min_days_remaining": 7}), } // 没订阅 ok, unmet, err := ev.Evaluate(context.Background(), tree, RuleContext{}) if err != nil { t.Fatalf("eval err: %v", err) } if ok || len(unmet) != 1 || unmet[0].Rule != RuleTypeHasSubscription { t.Fatalf("expected fail on no sub, got ok=%v unmet=%+v", ok, unmet) } // 有订阅但剩余不足 ok, unmet, err = ev.Evaluate(context.Background(), tree, RuleContext{ HasActiveSubscription: true, SubscriptionExpiresIn: 3 * 86400, }) if err != nil { t.Fatalf("eval err: %v", err) } if ok { t.Fatalf("expected fail on 3 days < 7 required, got pass") } if len(unmet) != 1 || unmet[0].Required != 7 || unmet[0].Current != 3 { t.Fatalf("unmet mismatch: %+v", unmet) } // 有订阅剩余足够 ok, _, err = ev.Evaluate(context.Background(), tree, RuleContext{ HasActiveSubscription: true, SubscriptionExpiresIn: 30 * 86400, }) if err != nil || !ok { t.Fatalf("expected pass on 30 days, got ok=%v err=%v", ok, err) } } func TestEvaluator_InviteCount(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{ Type: RuleTypeInviteCount, Params: mustParams(t, map[string]int64{"min": 3}), } ok, unmet, err := ev.Evaluate(context.Background(), tree, RuleContext{InviteCount: 1}) if err != nil || ok { t.Fatalf("expected fail, got ok=%v err=%v", ok, err) } if unmet[0].Current != 1 || unmet[0].Required != 3 { t.Fatalf("expected current=1 required=3, got %+v", unmet[0]) } ok, _, _ = ev.Evaluate(context.Background(), tree, RuleContext{InviteCount: 3}) if !ok { t.Fatalf("expected pass with InviteCount=3") } } func TestEvaluator_ANDShort(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{ Op: OpAND, Children: []*EligibilityRule{ {Type: RuleTypeHasSubscription}, {Type: RuleTypeInviteCount, Params: mustParams(t, map[string]int64{"min": 5})}, }, } ok, unmet, _ := ev.Evaluate(context.Background(), tree, RuleContext{ HasActiveSubscription: true, InviteCount: 1, }) if ok { t.Fatalf("expected AND to fail") } // AND 需要收集全部未通过项(这里只有 1 条) if len(unmet) != 1 || unmet[0].Rule != RuleTypeInviteCount { t.Fatalf("expected 1 unmet (invite_count), got %+v", unmet) } } func TestEvaluator_ORPassIgnoresChildUnmet(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{ Op: OpOR, Children: []*EligibilityRule{ {Type: RuleTypeInviteCount, Params: mustParams(t, map[string]int64{"min": 100})}, {Type: RuleTypeHasSubscription}, // 会通过 }, } ok, unmet, _ := ev.Evaluate(context.Background(), tree, RuleContext{ HasActiveSubscription: true, InviteCount: 0, }) if !ok { t.Fatalf("expected OR to pass because one child passes") } if len(unmet) != 0 { t.Fatalf("OR pass should hide child failures, got %+v", unmet) } } func TestEvaluator_ORFailBubblesAllChildren(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{ Op: OpOR, Children: []*EligibilityRule{ {Type: RuleTypeInviteCount, Params: mustParams(t, map[string]int64{"min": 5})}, {Type: RuleTypeHasSubscription}, }, } ok, unmet, _ := ev.Evaluate(context.Background(), tree, RuleContext{ InviteCount: 1, }) if ok { t.Fatalf("expected OR to fail") } if len(unmet) != 2 { t.Fatalf("expected 2 unmet reasons on OR fail, got %+v", unmet) } } func TestEvaluator_SubscriptionType(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{ Type: RuleTypeSubscriptionType, Params: mustParams(t, map[string][]int64{"plan_ids": {10, 20}}), } ok, _, _ := ev.Evaluate(context.Background(), tree, RuleContext{ SubscriptionPlanIds: []int64{20}, }) if !ok { t.Fatalf("expected pass when user plan matches") } ok, _, _ = ev.Evaluate(context.Background(), tree, RuleContext{ SubscriptionPlanIds: []int64{99}, }) if ok { t.Fatalf("expected fail when user plan not in whitelist") } } func TestEvaluator_UserTag(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{ Type: RuleTypeUserTag, Params: mustParams(t, map[string][]string{"tags": {"vip", "beta"}}), } ok, _, _ := ev.Evaluate(context.Background(), tree, RuleContext{UserTags: []string{"beta"}}) if !ok { t.Fatalf("expected pass when any tag matches") } ok, _, _ = ev.Evaluate(context.Background(), tree, RuleContext{UserTags: []string{"foo"}}) if ok { t.Fatalf("expected fail when no tags match") } } func TestEvaluator_NilTreeAlwaysPasses(t *testing.T) { ev := NewRuleEvaluator() ok, unmet, err := ev.Evaluate(context.Background(), nil, RuleContext{}) if err != nil || !ok || len(unmet) != 0 { t.Fatalf("nil tree should always pass; got ok=%v unmet=%+v err=%v", ok, unmet, err) } } func TestEvaluator_UnknownRuleType(t *testing.T) { ev := NewRuleEvaluator() tree := &EligibilityRule{Type: "no_such_rule"} _, _, err := ev.Evaluate(context.Background(), tree, RuleContext{}) if err == nil { t.Fatalf("expected error on unknown rule type") } }