1package mtastsdb
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net"
8 "net/http"
9 "os"
10 "path/filepath"
11 "reflect"
12 "testing"
13 "time"
14
15 "github.com/mjl-/mox/dns"
16 "github.com/mjl-/mox/mlog"
17 "github.com/mjl-/mox/mox-"
18 "github.com/mjl-/mox/mtasts"
19)
20
21func tcheckf(t *testing.T, err error, format string, args ...any) {
22 if err != nil {
23 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
24 }
25}
26
27func TestDB(t *testing.T) {
28 mox.Shutdown = ctxbg
29 mox.ConfigStaticPath = filepath.FromSlash("../testdata/mtasts/fake.conf")
30 mox.Conf.Static.DataDir = "."
31
32 dbpath := mox.DataDirPath("mtasts.db")
33 os.MkdirAll(filepath.Dir(dbpath), 0770)
34 os.Remove(dbpath)
35 defer os.Remove(dbpath)
36
37 log := mlog.New("mtastsdb", nil)
38
39 if err := Init(false); err != nil {
40 t.Fatalf("init database: %s", err)
41 }
42 defer Close()
43
44 // Mock time.
45 now := time.Now().Round(0)
46 timeNow = func() time.Time { return now }
47 defer func() { timeNow = time.Now }()
48
49 if p, err := lookup(ctxbg, log, dns.Domain{ASCII: "example.com"}); err != ErrNotFound {
50 t.Fatalf("expected not found, got %v, %#v", err, p)
51 }
52
53 policy1 := mtasts.Policy{
54 Version: "STSv1",
55 Mode: mtasts.ModeTesting,
56 MX: []mtasts.MX{
57 {Domain: dns.Domain{ASCII: "mx1.example.com"}},
58 {Domain: dns.Domain{ASCII: "mx2.example.com"}},
59 {Domain: dns.Domain{ASCII: "mx.backup-example.com"}},
60 },
61 MaxAgeSeconds: 1296000,
62 }
63 if err := Upsert(ctxbg, dns.Domain{ASCII: "example.com"}, "123", &policy1, policy1.String()); err != nil {
64 t.Fatalf("upsert record: %s", err)
65 }
66 if got, err := lookup(ctxbg, log, dns.Domain{ASCII: "example.com"}); err != nil {
67 t.Fatalf("lookup after insert: %s", err)
68 } else if !reflect.DeepEqual(got.Policy, policy1) {
69 t.Fatalf("mismatch between inserted and retrieved: got %#v, want %#v", got, policy1)
70 }
71
72 policy2 := mtasts.Policy{
73 Version: "STSv1",
74 Mode: mtasts.ModeEnforce,
75 MX: []mtasts.MX{
76 {Domain: dns.Domain{ASCII: "mx1.example.com"}},
77 },
78 MaxAgeSeconds: 360000,
79 }
80 if err := Upsert(ctxbg, dns.Domain{ASCII: "example.com"}, "124", &policy2, policy2.String()); err != nil {
81 t.Fatalf("upsert record: %s", err)
82 }
83 if got, err := lookup(ctxbg, log, dns.Domain{ASCII: "example.com"}); err != nil {
84 t.Fatalf("lookup after insert: %s", err)
85 } else if !reflect.DeepEqual(got.Policy, policy2) {
86 t.Fatalf("mismatch between inserted and retrieved: got %v, want %v", got, policy2)
87 }
88
89 // Check if database holds expected record.
90 records, err := PolicyRecords(ctxbg)
91 tcheckf(t, err, "policyrecords")
92 expRecords := []PolicyRecord{
93 {"example.com", now, now.Add(time.Duration(policy2.MaxAgeSeconds) * time.Second), now, now, false, "124", policy2, policy2.String()},
94 }
95 records[0].Policy = mtasts.Policy{}
96 expRecords[0].Policy = mtasts.Policy{}
97 if !reflect.DeepEqual(records, expRecords) {
98 t.Fatalf("records mismatch, got %#v, expected %#v", records, expRecords)
99 }
100
101 if err := Upsert(ctxbg, dns.Domain{ASCII: "other.example.com"}, "", nil, ""); err != nil {
102 t.Fatalf("upsert record: %s", err)
103 }
104 records, err = PolicyRecords(ctxbg)
105 tcheckf(t, err, "policyrecords")
106 policyNone := mtasts.Policy{Mode: mtasts.ModeNone, MaxAgeSeconds: 5 * 60}
107 expRecords = []PolicyRecord{
108 {"other.example.com", now, now.Add(5 * 60 * time.Second), now, now, true, "", policyNone, ""},
109 {"example.com", now, now.Add(time.Duration(policy2.MaxAgeSeconds) * time.Second), now, now, false, "124", policy2, policy2.String()},
110 }
111 if !reflect.DeepEqual(records, expRecords) {
112 t.Fatalf("records mismatch, got %#v, expected %#v", records, expRecords)
113 }
114
115 if _, err := lookup(ctxbg, log, dns.Domain{ASCII: "other.example.com"}); err != ErrBackoff {
116 t.Fatalf("got %#v, expected ErrBackoff", err)
117 }
118
119 resolver := dns.MockResolver{
120 TXT: map[string][]string{
121 "_mta-sts.example.com.": {"v=STSv1; id=124"},
122 "_mta-sts.other.example.com.": {"v=STSv1; id=1"},
123 "_mta-sts.temperror.example.com.": {""},
124 },
125 Fail: []string{
126 "txt _mta-sts.temperror.example.com.",
127 },
128 }
129
130 testGet := func(domain string, expPolicy *mtasts.Policy, expFresh bool, expErr error) {
131 t.Helper()
132 p, _, fresh, err := Get(ctxbg, log.Logger, resolver, dns.Domain{ASCII: domain})
133 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
134 t.Fatalf("got err %v, expected %v", err, expErr)
135 }
136 if !reflect.DeepEqual(p, expPolicy) || fresh != expFresh {
137 t.Fatalf("got policy %#v, fresh %v, expected %#v, %v", p, fresh, expPolicy, expFresh)
138 }
139 }
140
141 testGet("example.com", &policy2, true, nil)
142 testGet("other.example.com", nil, false, nil) // Back off, already in database.
143 testGet("absent.example.com", nil, true, nil) // No MTA-STS.
144 testGet("temperror.example.com", nil, false, mtasts.ErrDNS)
145
146 // Force refetch of policy, that will fail.
147 mtasts.HTTPClient.Transport = &http.Transport{
148 DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
149 return nil, fmt.Errorf("bad")
150 },
151 }
152 defer func() {
153 mtasts.HTTPClient.Transport = nil
154 }()
155 resolver.TXT["_mta-sts.example.com."] = []string{"v=STSv1; id=125"}
156 testGet("example.com", &policy2, false, nil)
157
158 // Cached policy but no longer a DNS record.
159 delete(resolver.TXT, "_mta-sts.example.com.")
160 testGet("example.com", &policy2, false, nil)
161}
162