15 "github.com/mjl-/mox/dns"
16 "github.com/mjl-/mox/mlog"
17 "github.com/mjl-/mox/mox-"
18 "github.com/mjl-/mox/mtasts"
21func tcheckf(t *testing.T, err error, format string, args ...any) {
23 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
27func TestDB(t *testing.T) {
29 mox.ConfigStaticPath = filepath.FromSlash("../testdata/mtasts/fake.conf")
30 mox.Conf.Static.DataDir = "."
32 dbpath := mox.DataDirPath("mtasts.db")
33 os.MkdirAll(filepath.Dir(dbpath), 0770)
35 defer os.Remove(dbpath)
37 log := mlog.New("mtastsdb", nil)
39 if err := Init(false); err != nil {
40 t.Fatalf("init database: %s", err)
45 now := time.Now().Round(0)
46 timeNow = func() time.Time { return now }
47 defer func() { timeNow = time.Now }()
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)
53 policy1 := mtasts.Policy{
55 Mode: mtasts.ModeTesting,
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"}},
61 MaxAgeSeconds: 1296000,
63 if err := Upsert(ctxbg, dns.Domain{ASCII: "example.com"}, "123", &policy1, policy1.String()); err != nil {
64 t.Fatalf("upsert record: %s", err)
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)
72 policy2 := mtasts.Policy{
74 Mode: mtasts.ModeEnforce,
76 {Domain: dns.Domain{ASCII: "mx1.example.com"}},
78 MaxAgeSeconds: 360000,
80 if err := Upsert(ctxbg, dns.Domain{ASCII: "example.com"}, "124", &policy2, policy2.String()); err != nil {
81 t.Fatalf("upsert record: %s", err)
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)
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()},
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)
101 if err := Upsert(ctxbg, dns.Domain{ASCII: "other.example.com"}, "", nil, ""); err != nil {
102 t.Fatalf("upsert record: %s", err)
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()},
111 if !reflect.DeepEqual(records, expRecords) {
112 t.Fatalf("records mismatch, got %#v, expected %#v", records, expRecords)
115 if _, err := lookup(ctxbg, log, dns.Domain{ASCII: "other.example.com"}); err != ErrBackoff {
116 t.Fatalf("got %#v, expected ErrBackoff", err)
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.": {""},
126 "txt _mta-sts.temperror.example.com.",
130 testGet := func(domain string, expPolicy *mtasts.Policy, expFresh bool, expErr error) {
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)
136 if !reflect.DeepEqual(p, expPolicy) || fresh != expFresh {
137 t.Fatalf("got policy %#v, fresh %v, expected %#v, %v", p, fresh, expPolicy, expFresh)
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)
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")
153 mtasts.HTTPClient.Transport = nil
155 resolver.TXT["_mta-sts.example.com."] = []string{"v=STSv1; id=125"}
156 testGet("example.com", &policy2, false, nil)
158 // Cached policy but no longer a DNS record.
159 delete(resolver.TXT, "_mta-sts.example.com.")
160 testGet("example.com", &policy2, false, nil)