package classify import "testing" func TestParseRulesSkipsCommentsAndBlanks(t *testing.T) { set := ParseRules(` # a comment # an indented comment from contains "amazon.com" => category=Newsletter, action=unsubscribe `) if len(set.Errors) != 0 { t.Fatalf("Errors = %v, want none", set.Errors) } if len(set.Rules) != 1 { t.Fatalf("Rules len = %d, want 1", len(set.Rules)) } r := set.Rules[0] if r.Line != 5 { t.Errorf("Line = %d, want 5", r.Line) } if r.Effects["category"] != "Newsletter" || r.Effects["action"] != "unsubscribe" { t.Errorf("Effects = %v", r.Effects) } } func TestParseRulesMatchAll(t *testing.T) { set := ParseRules(`* => category=Info`) if len(set.Rules) != 1 || !set.Rules[0].MatchAll { t.Fatalf("expected a match-all rule, got %+v (errors %v)", set.Rules, set.Errors) } if got := set.Match(map[string]string{"subject": "anything"}); got == nil { t.Fatal("match-all rule did not match") } } func TestParseRulesReportsBadLines(t *testing.T) { set := ParseRules(` from => category=Info from contains "x" => importance=urgent from contains "x" => bogus=1 missing the arrow `) if len(set.Rules) != 0 { t.Fatalf("Rules = %+v, want none", set.Rules) } if len(set.Errors) != 4 { t.Fatalf("Errors len = %d, want 4: %v", len(set.Errors), set.Errors) } } func TestRuleMatchAndPrecedence(t *testing.T) { set := ParseRules(` from ends_with "@stripe.com" and subject contains "invoice" => category=Action, importance=high from contains "stripe" => category=Newsletter * => category=Info `) if len(set.Errors) != 0 { t.Fatalf("unexpected errors: %v", set.Errors) } // First matching rule wins. got := set.Match(map[string]string{ "from": "Billing@Stripe.com", "subject": "Your invoice is ready", }) if got == nil || got.Effects["category"] != "Action" { t.Fatalf("first rule did not win: %+v", got) } // The second rule matches on its own terms. got = set.Match(map[string]string{"from": "stripe-press@example.com"}) if got == nil || got.Effects["category"] != "Newsletter" { t.Fatalf("second rule did not match: %+v", got) } // The catch-all is the fallback. got = set.Match(map[string]string{"from": "someone@example.com"}) if got == nil || got.Effects["category"] != "Info" { t.Fatalf("catch-all did not match: %+v", got) } } func TestRuleOperators(t *testing.T) { cases := []struct { name string rule string fields map[string]string wantCat string wantNone bool }{ {"equals", `from equals "a@b.com" => category=Action`, map[string]string{"from": "A@B.com"}, "Action", false}, {"starts_with", `subject starts_with "Hello" => category=Info`, map[string]string{"subject": "hello there"}, "Info", false}, {"ends_with", `source ends_with "where-woof.com" => category=Newsletter`, map[string]string{"source": "hello@where-woof.com"}, "Newsletter", false}, {"body", `body contains "unsubscribe here" => category=Newsletter`, map[string]string{"body": "Click UNSUBSCRIBE HERE to stop"}, "Newsletter", false}, {"no match", `from contains "zzz" => category=Action`, map[string]string{"from": "a@b.com"}, "", true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { set := ParseRules(tc.rule) if len(set.Errors) != 0 { t.Fatalf("parse errors: %v", set.Errors) } got := set.Match(tc.fields) if tc.wantNone { if got != nil { t.Fatalf("expected no match, got %+v", got) } return } if got == nil { t.Fatal("expected a match, got none") } if got.Effects["category"] != tc.wantCat { t.Errorf("category = %q, want %q", got.Effects["category"], tc.wantCat) } }) } }