1package spf
2
3import (
4 "net"
5 "reflect"
6 "testing"
7)
8
9func TestParse(t *testing.T) {
10 intptr := func(v int) *int {
11 return &v
12 }
13
14 mustParseIP := func(s string) net.IP {
15 ip := net.ParseIP(s)
16 if ip == nil {
17 t.Fatalf("bad ip %q", s)
18 }
19 return ip
20 }
21
22 test := func(txt string, expRecord *Record) {
23 t.Helper()
24 valid := expRecord != nil
25 r, _, err := ParseRecord(txt)
26 if valid && err != nil {
27 t.Fatalf("expected success, got err %s, txt %q", err, txt)
28 }
29 if !valid && err == nil {
30 t.Fatalf("expected error, got record %#v, txt %q", r, txt)
31 }
32 if valid && !reflect.DeepEqual(r, expRecord) {
33 t.Fatalf("unexpected record:\ngot: %v\nexpected: %v, txt %q", r, expRecord, txt)
34 }
35 }
36
37 test("", nil)
38 test("v=spf1", &Record{Version: "spf1"})
39 test("v=SPF1", &Record{Version: "spf1"})
40 test("V=spf1 ", &Record{Version: "spf1"})
41 test("V=spf1 all Include:example.org a ?a -a +a ~a a:x a:x/0 a:x/24//64 a:x//64 mx mx:x ptr ptr:x IP4:10.0.0.1 ip4:0.0.0.0/0 ip4:10.0.0.1/24 ip6:2001:db8::1 ip6:2001:db8::1/128 exists:x REDIRECT=x exp=X Other=x",
42 &Record{
43 Version: "spf1",
44 Directives: []Directive{
45 {Mechanism: "all"},
46 {Mechanism: "include", DomainSpec: "example.org"},
47 {Mechanism: "a"},
48 {Qualifier: "?", Mechanism: "a"},
49 {Qualifier: "-", Mechanism: "a"},
50 {Qualifier: "+", Mechanism: "a"},
51 {Qualifier: "~", Mechanism: "a"},
52 {Mechanism: "a", DomainSpec: "x"},
53 {Mechanism: "a", DomainSpec: "x", IP4CIDRLen: intptr(0)},
54 {Mechanism: "a", DomainSpec: "x", IP4CIDRLen: intptr(24), IP6CIDRLen: intptr(64)},
55 {Mechanism: "a", DomainSpec: "x", IP6CIDRLen: intptr(64)},
56 {Mechanism: "mx"},
57 {Mechanism: "mx", DomainSpec: "x"},
58 {Mechanism: "ptr"},
59 {Mechanism: "ptr", DomainSpec: "x"},
60 {Mechanism: "ip4", IP: mustParseIP("10.0.0.1"), IPstr: "10.0.0.1/32"},
61 {Mechanism: "ip4", IP: mustParseIP("0.0.0.0"), IPstr: "0.0.0.0/0", IP4CIDRLen: intptr(0)},
62 {Mechanism: "ip4", IP: mustParseIP("10.0.0.1"), IPstr: "10.0.0.1/24", IP4CIDRLen: intptr(24)},
63 {Mechanism: "ip6", IP: mustParseIP("2001:db8::1"), IPstr: "2001:db8::1/128"},
64 {Mechanism: "ip6", IP: mustParseIP("2001:db8::1"), IPstr: "2001:db8::1/128", IP6CIDRLen: intptr(128)},
65 {Mechanism: "exists", DomainSpec: "x"},
66 },
67 Redirect: "x",
68 Explanation: "X",
69 Other: []Modifier{
70 {"Other", "x"},
71 },
72 },
73 )
74 test("V=spf1 -all", &Record{Version: "spf1", Directives: []Directive{{Qualifier: "-", Mechanism: "all"}}})
75 test("v=spf1 !", nil) // Invalid character.
76 test("v=spf1 ?redirect=bogus", nil)
77 test("v=spf1 redirect=mox.example redirect=mox2.example", nil) // Duplicate redirect.
78 test("v=spf1 exp=mox.example exp=mox2.example", nil) // Duplicate exp.
79 test("v=spf1 ip4:10.0.0.256", nil) // Invalid address.
80 test("v=spf1 ip6:2001:db8:::1", nil) // Invalid address.
81 test("v=spf1 ip4:10.0.0.1/33", nil) // IPv4 prefix >32.
82 test("v=spf1 ip6:2001:db8::1/129", nil) // IPv6 prefix >128.
83 test("v=spf1 a:mox.example/33", nil) // IPv4 prefix >32.
84 test("v=spf1 a:mox.example//129", nil) // IPv6 prefix >128.
85 test("v=spf1 a:mox.example//129", nil) // IPv6 prefix >128.
86 test("v=spf1 exists:%%.%{l1r+}.%{d}",
87 &Record{
88 Version: "spf1",
89 Directives: []Directive{
90 {Mechanism: "exists", DomainSpec: "%%.%{l1r+}.%{d}"},
91 },
92 },
93 )
94 test("v=spf1 exists:%{l1r+}..", nil) // Empty toplabel in domain-end.
95 test("v=spf1 exists:%{l1r+}._.", nil) // Invalid toplabel in domain-end.
96 test("v=spf1 exists:%{l1r+}.123.", nil) // Invalid toplabel in domain-end.
97 test("v=spf1 exists:%{l1r+}.bad-.", nil) // Invalid toplabel in domain-end.
98 test("v=spf1 exists:%{l1r+}.-bad.", nil) // Invalid toplabel in domain-end.
99 test("v=spf1 exists:%{l1r+}./.", nil) // Invalid toplabel in domain-end.
100 test("v=spf1 exists:%{x}", nil) // Unknown macro-letter.
101 test("v=spf1 exists:%{s0}", nil) // Invalid digits.
102 test("v=spf1 exists:%{ir}.%{l1r+}.%{d}",
103 &Record{
104 Version: "spf1",
105 Directives: []Directive{
106 {Mechanism: "exists", DomainSpec: "%{ir}.%{l1r+}.%{d}"},
107 },
108 },
109 )
110
111 orig := `V=SPF1 all Include:example.org a ?a -a +a ~a a:x a:x/0 a:x/24//64 a:x//64 mx mx:x ptr ptr:x IP4:10.0.0.1 ip4:0.0.0.0/0 ip4:10.0.0.1/24 ip6:2001:db8::1 ip6:2001:db8::1/128 exists:x REDIRECT=x exp=X Other=x`
112 exp := `v=spf1 all include:example.org a ?a -a +a ~a a:x a:x/0 a:x/24//64 a:x//64 mx mx:x ptr ptr:x ip4:10.0.0.1 ip4:0.0.0.0/0 ip4:10.0.0.1/24 ip6:2001:db8::1 ip6:2001:db8::1/128 exists:x redirect=x exp=X Other=x`
113 r, _, err := ParseRecord(orig)
114 if err != nil {
115 t.Fatalf("parsing original: %s", err)
116 }
117 record, err := r.Record()
118 if err != nil {
119 t.Fatalf("making dns record: %s", err)
120 }
121 if record != exp {
122 t.Fatalf("packing dns record, got %q, expected %q", record, exp)
123 }
124}
125
126func FuzzParseRecord(f *testing.F) {
127 f.Add("")
128 f.Add("v=spf1")
129 f.Add(`V=SPF1 all Include:example.org a ?a -a +a ~a a:x a:x/0 a:x/24//64 a:x//64 mx mx:x ptr ptr:x IP4:10.0.0.1 ip4:0.0.0.0/0 ip4:10.0.0.1/24 ip6:2001:db8::1 ip6:2001:db8::1/128 exists:x REDIRECT=x exp=X Other=x`)
130 f.Fuzz(func(t *testing.T, s string) {
131 r, _, err := ParseRecord(s)
132 if err == nil {
133 if _, err := r.Record(); err != nil {
134 t.Errorf("r.Record for %s, %#v: %s", s, r, err)
135 }
136 }
137 })
138}
139