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() }