feat(rules): add Rule interface, Pipeline, and WhitelistRule
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPipeline_NoRules_Accepts(t *testing.T) {
|
||||
p := NewPipeline(nil)
|
||||
result := p.Evaluate(EmailContext{Sender: "anyone@test.com"})
|
||||
if !result.Accepted {
|
||||
t.Fatal("expected accept with no rules")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_AllRulesAccept_Accepts(t *testing.T) {
|
||||
allowAll := &stubRule{name: "allow-all", result: Accept()}
|
||||
p := NewPipeline([]Rule{allowAll})
|
||||
result := p.Evaluate(EmailContext{Sender: "a@b.com"})
|
||||
if !result.Accepted {
|
||||
t.Fatal("expected accept")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_OneRuleRejects_Rejects(t *testing.T) {
|
||||
rejector := &stubRule{name: "rejector", result: Reject("nope")}
|
||||
p := NewPipeline([]Rule{rejector})
|
||||
result := p.Evaluate(EmailContext{Sender: "a@b.com"})
|
||||
if result.Accepted {
|
||||
t.Fatal("expected reject")
|
||||
}
|
||||
if result.Reason != "nope" {
|
||||
t.Fatalf("expected reason 'nope', got %s", result.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_MergesMetadata(t *testing.T) {
|
||||
r1 := &stubRule{name: "r1", result: Accept().WithMetadata("a", "1")}
|
||||
r2 := &stubRule{name: "r2", result: Accept().WithMetadata("b", "2")}
|
||||
p := NewPipeline([]Rule{r1, r2})
|
||||
result := p.Evaluate(EmailContext{})
|
||||
if result.Metadata["a"] != "1" {
|
||||
t.Fatal("missing metadata a")
|
||||
}
|
||||
if result.Metadata["b"] != "2" {
|
||||
t.Fatal("missing metadata b")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipeline_StopsOnFirstReject(t *testing.T) {
|
||||
rejector := &stubRule{name: "rejector", result: Reject("stop")}
|
||||
neverCalled := &trackingRule{}
|
||||
p := NewPipeline([]Rule{rejector, neverCalled})
|
||||
p.Evaluate(EmailContext{})
|
||||
if neverCalled.called {
|
||||
t.Fatal("expected second rule not to be called after rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistRule_EmptyList_Accepts(t *testing.T) {
|
||||
r := NewWhitelistRule(nil)
|
||||
result := r.Evaluate(EmailContext{Sender: "anyone@test.com"})
|
||||
if !result.Accepted {
|
||||
t.Fatal("empty whitelist should accept all")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistRule_SenderInList_Accepts(t *testing.T) {
|
||||
r := NewWhitelistRule([]string{"allowed@test.com"})
|
||||
result := r.Evaluate(EmailContext{Sender: "allowed@test.com"})
|
||||
if !result.Accepted {
|
||||
t.Fatal("expected accept for whitelisted sender")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistRule_SenderNotInList_Rejects(t *testing.T) {
|
||||
r := NewWhitelistRule([]string{"allowed@test.com"})
|
||||
result := r.Evaluate(EmailContext{Sender: "unknown@test.com"})
|
||||
if result.Accepted {
|
||||
t.Fatal("expected reject for non-whitelisted sender")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistRule_CaseInsensitive(t *testing.T) {
|
||||
r := NewWhitelistRule([]string{"Allowed@Test.com"})
|
||||
result := r.Evaluate(EmailContext{Sender: "allowed@test.com"})
|
||||
if !result.Accepted {
|
||||
t.Fatal("whitelist should be case-insensitive")
|
||||
}
|
||||
}
|
||||
|
||||
// stubs
|
||||
|
||||
type stubRule struct {
|
||||
name string
|
||||
result RuleResult
|
||||
}
|
||||
|
||||
func (s *stubRule) Name() string { return s.name }
|
||||
func (s *stubRule) Evaluate(_ EmailContext) RuleResult { return s.result }
|
||||
|
||||
type trackingRule struct {
|
||||
called bool
|
||||
}
|
||||
|
||||
func (t *trackingRule) Name() string { return "tracker" }
|
||||
func (t *trackingRule) Evaluate(_ EmailContext) RuleResult { t.called = true; return Accept() }
|
||||
Reference in New Issue
Block a user