All checks were successful
continuous-integration/drone/push Build is passing
Closes: https://framagit.org/happyDomain/happydeliver/-/issues/1
284 lines
8 KiB
Go
284 lines
8 KiB
Go
// This file is part of the happyDeliver (R) project.
|
|
// Copyright (c) 2025 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package analyzer
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestValidateSPF(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record string
|
|
expectError bool
|
|
errorMsg string // Expected error message (substring match)
|
|
}{
|
|
{
|
|
name: "Valid SPF with -all",
|
|
record: "v=spf1 include:_spf.example.com -all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with ~all",
|
|
record: "v=spf1 ip4:192.0.2.0/24 ~all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with +all",
|
|
record: "v=spf1 +all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with ?all",
|
|
record: "v=spf1 mx ?all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with redirect",
|
|
record: "v=spf1 redirect=_spf.example.com",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with redirect and mechanisms",
|
|
record: "v=spf1 ip4:192.0.2.0/24 redirect=_spf.example.com",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with multiple mechanisms",
|
|
record: "v=spf1 a mx ip4:192.0.2.0/24 include:_spf.example.com -all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with exp modifier",
|
|
record: "v=spf1 mx exp=explain.example.com -all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Invalid SPF - no version",
|
|
record: "include:_spf.example.com -all",
|
|
expectError: true,
|
|
errorMsg: "must start with 'v=spf1'",
|
|
},
|
|
{
|
|
name: "Invalid SPF - no all mechanism or redirect",
|
|
record: "v=spf1 include:_spf.example.com",
|
|
expectError: true,
|
|
errorMsg: "should end with an 'all' mechanism",
|
|
},
|
|
{
|
|
name: "Invalid SPF - wrong version",
|
|
record: "v=spf2 include:_spf.example.com -all",
|
|
expectError: true,
|
|
errorMsg: "must start with 'v=spf1'",
|
|
},
|
|
{
|
|
name: "Invalid SPF - include= instead of include:",
|
|
record: "v=spf1 include=icloud.com ~all",
|
|
expectError: true,
|
|
errorMsg: "should use ':' not '='",
|
|
},
|
|
{
|
|
name: "Invalid SPF - a= instead of a:",
|
|
record: "v=spf1 a=example.com -all",
|
|
expectError: true,
|
|
errorMsg: "should use ':' not '='",
|
|
},
|
|
{
|
|
name: "Invalid SPF - mx= instead of mx:",
|
|
record: "v=spf1 mx=example.com -all",
|
|
expectError: true,
|
|
errorMsg: "should use ':' not '='",
|
|
},
|
|
{
|
|
name: "Invalid SPF - unknown mechanism",
|
|
record: "v=spf1 foobar -all",
|
|
expectError: true,
|
|
errorMsg: "unknown mechanism",
|
|
},
|
|
{
|
|
name: "Invalid SPF - unknown modifier",
|
|
record: "v=spf1 -all unknown=value",
|
|
expectError: true,
|
|
errorMsg: "unknown modifier",
|
|
},
|
|
{
|
|
name: "Valid SPF with RFC 6652 ra modifier",
|
|
record: "v=spf1 mx ra=postmaster -all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with RFC 6652 rp modifier",
|
|
record: "v=spf1 mx rp=100 -all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with RFC 6652 rr modifier",
|
|
record: "v=spf1 mx rr=all -all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with all RFC 6652 modifiers",
|
|
record: "v=spf1 mx ra=postmaster rp=50 rr=fail -all",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Valid SPF with RFC 6652 modifiers and redirect",
|
|
record: "v=spf1 ip4:192.0.2.0/24 ra=abuse redirect=_spf.example.com",
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
analyzer := NewDNSAnalyzer(5 * time.Second)
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test as main record (isMainRecord = true) since these tests check overall SPF validity
|
|
err := analyzer.validateSPF(tt.record, true)
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("validateSPF(%q) expected error but got nil", tt.record)
|
|
} else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
|
|
t.Errorf("validateSPF(%q) error = %q, want error containing %q", tt.record, err.Error(), tt.errorMsg)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("validateSPF(%q) unexpected error: %v", tt.record, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateSPF_IncludedRecords(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record string
|
|
isMainRecord bool
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "Main record without 'all' - should error",
|
|
record: "v=spf1 include:_spf.example.com",
|
|
isMainRecord: true,
|
|
expectError: true,
|
|
errorMsg: "should end with an 'all' mechanism",
|
|
},
|
|
{
|
|
name: "Included record without 'all' - should NOT error",
|
|
record: "v=spf1 include:_spf.example.com",
|
|
isMainRecord: false,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Included record with only mechanisms - should NOT error",
|
|
record: "v=spf1 ip4:192.0.2.0/24 mx",
|
|
isMainRecord: false,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Main record with only mechanisms - should error",
|
|
record: "v=spf1 ip4:192.0.2.0/24 mx",
|
|
isMainRecord: true,
|
|
expectError: true,
|
|
errorMsg: "should end with an 'all' mechanism",
|
|
},
|
|
{
|
|
name: "Included record with 'all' - valid",
|
|
record: "v=spf1 ip4:192.0.2.0/24 -all",
|
|
isMainRecord: false,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "Main record with 'all' - valid",
|
|
record: "v=spf1 ip4:192.0.2.0/24 -all",
|
|
isMainRecord: true,
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
analyzer := NewDNSAnalyzer(5 * time.Second)
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := analyzer.validateSPF(tt.record, tt.isMainRecord)
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("validateSPF(%q, isMainRecord=%v) expected error but got nil", tt.record, tt.isMainRecord)
|
|
} else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
|
|
t.Errorf("validateSPF(%q, isMainRecord=%v) error = %q, want error containing %q", tt.record, tt.isMainRecord, err.Error(), tt.errorMsg)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("validateSPF(%q, isMainRecord=%v) unexpected error: %v", tt.record, tt.isMainRecord, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractSPFRedirect(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record string
|
|
expectedRedirect string
|
|
}{
|
|
{
|
|
name: "SPF with redirect",
|
|
record: "v=spf1 redirect=_spf.example.com",
|
|
expectedRedirect: "_spf.example.com",
|
|
},
|
|
{
|
|
name: "SPF with redirect and other mechanisms",
|
|
record: "v=spf1 ip4:192.0.2.0/24 redirect=_spf.google.com",
|
|
expectedRedirect: "_spf.google.com",
|
|
},
|
|
{
|
|
name: "SPF without redirect",
|
|
record: "v=spf1 include:_spf.example.com -all",
|
|
expectedRedirect: "",
|
|
},
|
|
{
|
|
name: "SPF with only all mechanism",
|
|
record: "v=spf1 -all",
|
|
expectedRedirect: "",
|
|
},
|
|
{
|
|
name: "Empty record",
|
|
record: "",
|
|
expectedRedirect: "",
|
|
},
|
|
}
|
|
|
|
analyzer := NewDNSAnalyzer(5 * time.Second)
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := analyzer.extractSPFRedirect(tt.record)
|
|
if result != tt.expectedRedirect {
|
|
t.Errorf("extractSPFRedirect(%q) = %q, want %q", tt.record, result, tt.expectedRedirect)
|
|
}
|
|
})
|
|
}
|
|
}
|