119 lines
3.0 KiB
Go
119 lines
3.0 KiB
Go
package rules
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
|
|
)
|
|
|
|
func TestRuleEvaluator(t *testing.T) {
|
|
eval := NewEvaluator()
|
|
|
|
entry := models.FeedEntry{
|
|
ID: "entry-1",
|
|
FeedID: "feed-hn",
|
|
FeedTitle: "Hacker News",
|
|
Title: "Announcing Go 1.24 Release",
|
|
Author: "Go Team",
|
|
Content: "<p>Go 1.24 brings new features...</p>",
|
|
Variants: map[string]models.EntryVariant{
|
|
"keywords": {
|
|
EntryID: "entry-1",
|
|
VariantKey: "keywords",
|
|
ContentType: "json",
|
|
TextValue: `["go", "compiler", "release"]`,
|
|
},
|
|
},
|
|
PublishedAt: time.Now().Add(-2 * time.Hour),
|
|
CreatedAt: time.Now().Add(-2 * time.Hour),
|
|
}
|
|
|
|
// Test 1: Title contains Go AND FeedID in list
|
|
rule1 := models.Rule{
|
|
ID: "r1",
|
|
Name: "Tech Feeds in list",
|
|
MatchMode: "all",
|
|
Conditions: []models.RuleCondition{
|
|
{Field: "feed_id", Operator: "in", Value: []string{"feed-hn", "feed-36kr", "feed-ft"}},
|
|
{Field: "title", Operator: "contains", Value: "Go 1.24"},
|
|
},
|
|
}
|
|
if !eval.Evaluate(rule1, entry) {
|
|
t.Error("expected rule1 to match entry")
|
|
}
|
|
|
|
// Test 2: not_in operator
|
|
rule2 := models.Rule{
|
|
ID: "r2",
|
|
Name: "Feed not in noise",
|
|
MatchMode: "all",
|
|
Conditions: []models.RuleCondition{
|
|
{Field: "feed_id", Operator: "not_in", Value: "feed-spam,feed-ads"},
|
|
},
|
|
}
|
|
if !eval.Evaluate(rule2, entry) {
|
|
t.Error("expected rule2 to match entry")
|
|
}
|
|
|
|
// Test 3: contains_any operator
|
|
rule3 := models.Rule{
|
|
ID: "r3",
|
|
Name: "Title match any keyword",
|
|
MatchMode: "all",
|
|
Conditions: []models.RuleCondition{
|
|
{Field: "title", Operator: "contains_any", Value: "Rust, Python, Go 1.24"},
|
|
},
|
|
}
|
|
if !eval.Evaluate(rule3, entry) {
|
|
t.Error("expected rule3 to match entry with contains_any")
|
|
}
|
|
|
|
// Test 4: exists operator on variants
|
|
rule4 := models.Rule{
|
|
ID: "r4",
|
|
Name: "Keywords exist",
|
|
MatchMode: "all",
|
|
Conditions: []models.RuleCondition{
|
|
{Field: "variants.keywords", Operator: "exists"},
|
|
{Field: "variants.nonexistent", Operator: "not_exists"},
|
|
},
|
|
}
|
|
if !eval.Evaluate(rule4, entry) {
|
|
t.Error("expected rule4 to evaluate variant existence correctly")
|
|
}
|
|
|
|
// Test 5: Timestamp within_last_hours
|
|
rule5 := models.Rule{
|
|
ID: "r5",
|
|
Name: "Recent article",
|
|
MatchMode: "all",
|
|
Conditions: []models.RuleCondition{
|
|
{Field: "published_at", Operator: "within_last_hours", Value: 24},
|
|
},
|
|
}
|
|
if !eval.Evaluate(rule5, entry) {
|
|
t.Error("expected rule5 to match article within last 24h")
|
|
}
|
|
|
|
// Test 6: Safety validator
|
|
unsafeRule := models.Rule{
|
|
Trigger: "variant_change",
|
|
Conditions: []models.RuleCondition{
|
|
{Field: "variants.tags", Operator: "exists"},
|
|
},
|
|
Actions: []models.RuleAction{
|
|
{
|
|
Type: "set_variant",
|
|
Params: map[string]interface{}{
|
|
"variant_key": "tags",
|
|
"value": "new-tag",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if err := ValidateRuleSafety(unsafeRule); err == nil {
|
|
t.Error("expected ValidateRuleSafety to detect cyclic loop")
|
|
}
|
|
}
|