maddy/msgpipeline/check_test.go
fox.cpp 028d8b914c
Rename dispatcher to "msgpipeline"
New name more precisely describes what it is doing now. It was initally
meant to be more generic and usable for other purposes, but I don't
think we will need that flexibility.
2019-10-13 18:42:51 +03:00

700 lines
20 KiB
Go

package msgpipeline
import (
"errors"
"testing"
"github.com/emersion/go-message/textproto"
"github.com/emersion/go-msgauth/authres"
"github.com/foxcpp/maddy/module"
"github.com/foxcpp/maddy/testutils"
)
func TestMsgPipeline_NoScoresChecked(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}, testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
},
Log: testutils.Logger(t, "msgpipeline"),
}
// No rejectScore or quarantineScore.
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
if target.Messages[0].MsgMeta.Quarantine {
t.Fatalf("message is quarantined when it shouldn't")
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_RejectScore(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}, testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}
rejectScore := 10
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
rejectScore: &rejectScore,
},
Log: testutils.Logger(t, "msgpipeline"),
}
// Should be rejected.
if _, err := testutils.DoTestDeliveryErr(t, &d, "whatever@whatever", []string{"whatever@whatever"}); err == nil {
t.Fatalf("expected an error")
}
if len(target.Messages) != 0 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 0, len(target.Messages))
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_RejectScore_notEnough(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}, testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}
rejectScore := 15
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
rejectScore: &rejectScore,
},
Log: testutils.Logger(t, "msgpipeline"),
}
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
if target.Messages[0].MsgMeta.Quarantine {
t.Fatalf("message is quarantined when it shouldn't")
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_Quarantine(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{},
}, testutils.Check{
BodyRes: module.CheckResult{Quarantine: true},
}
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
},
Log: testutils.Logger(t, "msgpipeline"),
}
// Should be quarantined.
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
if !target.Messages[0].MsgMeta.Quarantine {
t.Fatalf("message is not quarantined when it should")
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_QuarantineScore(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}, testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}
quarantineScore := 10
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
quarantineScore: &quarantineScore,
},
Log: testutils.Logger(t, "msgpipeline"),
}
// Should be quarantined.
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
if !target.Messages[0].MsgMeta.Quarantine {
t.Fatalf("message is not quarantined when it should")
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_QuarantineScore_notEnough(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}, testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}
quarantineScore := 15
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
quarantineScore: &quarantineScore,
},
Log: testutils.Logger(t, "msgpipeline"),
}
// Should be quarantined.
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
if target.Messages[0].MsgMeta.Quarantine {
t.Fatalf("message is quarantined when it shouldn't")
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_BothScores_Quarantined(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}, testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}
quarantineScore := 10
rejectScore := 15
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
quarantineScore: &quarantineScore,
rejectScore: &rejectScore,
},
Log: testutils.Logger(t, "msgpipeline"),
}
// Should be quarantined.
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
if !target.Messages[0].MsgMeta.Quarantine {
t.Fatalf("message is not quarantined when it should")
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_BothScores_Rejected(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}, testutils.Check{
BodyRes: module.CheckResult{ScoreAdjust: 5},
}
quarantineScore := 5
rejectScore := 10
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
quarantineScore: &quarantineScore,
rejectScore: &rejectScore,
},
Log: testutils.Logger(t, "msgpipeline"),
}
// Should be quarantined.
if _, err := testutils.DoTestDeliveryErr(t, &d, "whatever@whatever", []string{"whatever@whatever"}); err == nil {
t.Fatalf("message not rejected")
}
if len(target.Messages) != 0 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 0, len(target.Messages))
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_AuthResults(t *testing.T) {
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{
AuthResult: []authres.Result{
&authres.SPFResult{
Value: authres.ResultFail,
From: "FROM",
Helo: "HELO",
},
},
},
}, testutils.Check{
BodyRes: module.CheckResult{
AuthResult: []authres.Result{
&authres.SPFResult{
Value: authres.ResultFail,
From: "FROM2",
Helo: "HELO2",
},
},
},
}
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
},
Hostname: "TEST-HOST",
Log: testutils.Logger(t, "msgpipeline"),
}
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
authRes := target.Messages[0].Header.Get("Authentication-Results")
id, parsed, err := authres.Parse(authRes)
if err != nil {
t.Fatalf("failed to parse results")
}
if id != "TEST-HOST" {
t.Fatalf("wrong authres identifier")
}
if len(parsed) != 2 {
t.Fatalf("wrong amount of parts, want %d, got %d", 2, len(parsed))
}
var seen1, seen2 bool
for _, parts := range parsed {
spfPart, ok := parts.(*authres.SPFResult)
if !ok {
t.Fatalf("Not SPFResult")
}
if spfPart.From == "FROM" {
seen1 = true
}
if spfPart.From == "FROM2" {
seen2 = true
}
}
if !seen1 {
t.Fatalf("First authRes is missing")
}
if !seen2 {
t.Fatalf("Second authRes is missing")
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_Headers(t *testing.T) {
hdr1 := textproto.Header{}
hdr1.Add("HDR1", "1")
hdr2 := textproto.Header{}
hdr2.Add("HDR2", "2")
target := testutils.Target{}
check1, check2 := testutils.Check{
BodyRes: module.CheckResult{
Header: hdr1,
},
}, testutils.Check{
BodyRes: module.CheckResult{
Header: hdr2,
},
}
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check1, &check2},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
},
Hostname: "TEST-HOST",
Log: testutils.Logger(t, "msgpipeline"),
}
testutils.DoTestDelivery(t, &d, "whatever@whatever", []string{"whatever@whatever"})
if len(target.Messages) != 1 {
t.Fatalf("wrong amount of messages received, want %d, got %d", 1, len(target.Messages))
}
if target.Messages[0].Header.Get("HDR1") != "1" {
t.Fatalf("wrong HDR1 value, want %s, got %s", "1", target.Messages[0].Header.Get("HDR1"))
}
if target.Messages[0].Header.Get("HDR2") != "2" {
t.Fatalf("wrong HDR2 value, want %s, got %s", "1", target.Messages[0].Header.Get("HDR2"))
}
if check1.UnclosedStates != 0 || check2.UnclosedStates != 0 {
t.Fatalf("checks state objects leak or double-closed, alive counters: %v, %v", check1.UnclosedStates, check2.UnclosedStates)
}
}
func TestMsgPipeline_Globalcheck_Errors(t *testing.T) {
target := testutils.Target{}
check_ := testutils.Check{
InitErr: errors.New("1"),
ConnRes: module.CheckResult{RejectErr: errors.New("2")},
SenderRes: module.CheckResult{RejectErr: errors.New("3")},
RcptRes: module.CheckResult{RejectErr: errors.New("4")},
BodyRes: module.CheckResult{RejectErr: errors.New("5")},
}
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&check_},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
},
Hostname: "TEST-HOST",
Log: testutils.Logger(t, "msgpipeline"),
}
t.Run("init err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.InitErr = nil
t.Run("conn err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.ConnRes.RejectErr = nil
t.Run("mail from err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.SenderRes.RejectErr = nil
t.Run("rcpt to err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.RcptRes.RejectErr = nil
t.Run("body err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.BodyRes.RejectErr = nil
t.Run("no err", func(t *testing.T) {
testutils.DoTestDelivery(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
})
if check_.UnclosedStates != 0 {
t.Fatalf("check state objects leak or double-closed, counters: %d", check_.UnclosedStates)
}
}
func TestMsgPipeline_SourceCheck_Errors(t *testing.T) {
target := testutils.Target{}
check_ := testutils.Check{
InitErr: errors.New("1"),
ConnRes: module.CheckResult{RejectErr: errors.New("2")},
SenderRes: module.CheckResult{RejectErr: errors.New("3")},
RcptRes: module.CheckResult{RejectErr: errors.New("4")},
BodyRes: module.CheckResult{RejectErr: errors.New("5")},
}
globalCheck := testutils.Check{}
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&globalCheck},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
checks: []module.Check{&check_},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
},
Hostname: "TEST-HOST",
Log: testutils.Logger(t, "msgpipeline"),
}
t.Run("init err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.InitErr = nil
t.Run("conn err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.ConnRes.RejectErr = nil
t.Run("mail from err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.SenderRes.RejectErr = nil
t.Run("rcpt to err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.RcptRes.RejectErr = nil
t.Run("body err", func(t *testing.T) {
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.BodyRes.RejectErr = nil
t.Run("no err", func(t *testing.T) {
testutils.DoTestDelivery(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
})
if check_.UnclosedStates != 0 || globalCheck.UnclosedStates != 0 {
t.Fatalf("check state objects leak or double-closed, counters: %d, %d",
check_.UnclosedStates, globalCheck.UnclosedStates)
}
}
func TestMsgPipeline_RcptCheck_Errors(t *testing.T) {
target := testutils.Target{}
check_ := testutils.Check{
InitErr: errors.New("1"),
ConnRes: module.CheckResult{RejectErr: errors.New("2")},
SenderRes: module.CheckResult{RejectErr: errors.New("3")},
RcptRes: module.CheckResult{RejectErr: errors.New("4")},
BodyRes: module.CheckResult{RejectErr: errors.New("5")},
InstName: "err_check",
}
// Added to check whether it leaks.
globalCheck := testutils.Check{InstName: "global_check"}
sourceCheck := testutils.Check{InstName: "source_check"}
d := MsgPipeline{
msgpipelineCfg: msgpipelineCfg{
globalChecks: []module.Check{&globalCheck},
perSource: map[string]sourceBlock{},
defaultSource: sourceBlock{
perRcpt: map[string]*rcptBlock{},
checks: []module.Check{&check_},
defaultRcpt: &rcptBlock{
targets: []module.DeliveryTarget{&target},
},
},
},
Hostname: "TEST-HOST",
Log: testutils.Logger(t, "msgpipeline"),
}
t.Run("init err", func(t *testing.T) {
d.Log = testutils.Logger(t, "msgpipeline")
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
t.Log("!!!", check_.UnclosedStates)
})
check_.InitErr = nil
t.Run("conn err", func(t *testing.T) {
d.Log = testutils.Logger(t, "msgpipeline")
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
t.Log("!!!", check_.UnclosedStates)
})
check_.ConnRes.RejectErr = nil
t.Run("mail from err", func(t *testing.T) {
d.Log = testutils.Logger(t, "msgpipeline")
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
t.Log("!!!", check_.UnclosedStates)
})
check_.SenderRes.RejectErr = nil
t.Run("rcpt to err", func(t *testing.T) {
d.Log = testutils.Logger(t, "msgpipeline")
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.RcptRes.RejectErr = nil
t.Run("body err", func(t *testing.T) {
d.Log = testutils.Logger(t, "msgpipeline")
_, err := testutils.DoTestDeliveryErr(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
if err == nil {
t.Fatal("expected error")
}
})
check_.BodyRes.RejectErr = nil
t.Run("no err", func(t *testing.T) {
d.Log = testutils.Logger(t, "msgpipeline")
testutils.DoTestDelivery(t, &d, "sender@example.com", []string{"rcpt1@example.com", "rcpt2@example.com"})
})
if check_.UnclosedStates != 0 || sourceCheck.UnclosedStates != 0 || globalCheck.UnclosedStates != 0 {
t.Fatalf("check state objects leak or double-closed, counters: %d, %d, %d",
check_.UnclosedStates, sourceCheck.UnclosedStates, globalCheck.UnclosedStates)
}
}