1package updates
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/json"
7 "errors"
8 "io"
9 golog "log"
10 "net/http"
11 "net/http/httptest"
12 "reflect"
13 "testing"
14
15 "github.com/mjl-/mox/dns"
16 "github.com/mjl-/mox/mlog"
17)
18
19func TestUpdates(t *testing.T) {
20 log := mlog.New("updates", nil)
21
22 resolver := dns.MockResolver{
23 TXT: map[string][]string{
24 "_updates.mox.example.": {"v=UPDATES0; l=v0.0.1"},
25 "_updates.one.example.": {"other", "v=UPDATES0; l=v0.0.1-rc1"},
26 "_updates.dup.example.": {"v=UPDATES0; l=v0.0.1", "v=UPDATES0; l=v0.0.1"},
27 "_updates.other.example.": {"other"},
28 "_updates.malformed.example.": {"v=UPDATES0; l=bogus"},
29 "_updates.malformed2.example.": {"v=UPDATES0; bogus"},
30 "_updates.malformed3.example.": {"v=UPDATES0; l=v0.0.1; l=v0.0.1"},
31 "_updates.temperror.example.": {"v=UPDATES0; l=v0.0.1"},
32 "_updates.unknown.example.": {"v=UPDATES0; l=v0.0.1; unknown=ok"},
33 },
34 Fail: []string{
35 "txt _updates.temperror.example.",
36 },
37 }
38
39 lookup := func(dom string, expVersion string, expRecord *Record, expErr error) {
40 t.Helper()
41
42 d, _ := dns.ParseDomain(dom)
43 expv, _ := ParseVersion(expVersion)
44
45 version, record, err := Lookup(context.Background(), log.Logger, resolver, d)
46 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
47 t.Fatalf("lookup: got err %v, expected %v", err, expErr)
48 }
49 if version != expv || !reflect.DeepEqual(record, expRecord) {
50 t.Fatalf("lookup: got version %v, record %#v, expected %v %#v", version, record, expv, expRecord)
51 }
52 }
53
54 lookup("mox.example", "v0.0.1", &Record{Version: "UPDATES0", Latest: Version{0, 0, 1}}, nil)
55 lookup("one.example", "v0.0.1", &Record{Version: "UPDATES0", Latest: Version{0, 0, 1}}, nil)
56 lookup("absent.example", "", nil, ErrNoRecord)
57 lookup("dup.example", "", nil, ErrMultipleRecords)
58 lookup("other.example", "", nil, ErrNoRecord)
59 lookup("malformed.example", "", nil, ErrRecordSyntax)
60 lookup("malformed2.example", "", nil, ErrRecordSyntax)
61 lookup("malformed3.example", "", nil, ErrRecordSyntax)
62 lookup("temperror.example", "", nil, ErrDNS)
63 lookup("unknown.example", "v0.0.1", &Record{Version: "UPDATES0", Latest: Version{0, 0, 1}}, nil)
64
65 seed := make([]byte, ed25519.SeedSize)
66 priv := ed25519.NewKeyFromSeed(seed)
67 pub := []byte(priv.Public().(ed25519.PublicKey))
68 changelog := Changelog{
69 Changes: []Change{
70 {
71 PubKey: pub,
72 Sig: ed25519.Sign(priv, []byte("test")),
73 Text: "test",
74 },
75 },
76 }
77
78 fetch := func(baseURL string, version Version, status int, pubKey []byte, expChangelog *Changelog, expErr error) {
79 t.Helper()
80
81 mux := &http.ServeMux{}
82 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
83 if status == 0 {
84 panic("bad serve")
85 }
86 w.WriteHeader(status)
87 err := json.NewEncoder(w).Encode(changelog)
88 if err != nil {
89 t.Fatalf("encode changelog: %v", err)
90 }
91 })
92 s := httptest.NewUnstartedServer(mux)
93 s.Config.ErrorLog = golog.New(io.Discard, "", 0)
94 s.Start()
95 defer s.Close()
96 if baseURL == "" {
97 baseURL = s.URL
98 }
99
100 changelog, err := FetchChangelog(context.Background(), log.Logger, baseURL, version, pubKey)
101 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
102 t.Fatalf("fetch changelog: got err %v, expected %v", err, expErr)
103 }
104 if !reflect.DeepEqual(changelog, expChangelog) {
105 t.Fatalf("fetch changelog: got changelog %v, expected %v", changelog, expChangelog)
106 }
107 }
108
109 fetch("", Version{}, 200, pub, &changelog, nil)
110 fetch("", Version{1, 1, 1}, 200, pub, &changelog, nil)
111 fetch("", Version{}, 200, make([]byte, ed25519.PublicKeySize), nil, ErrChangelogFetch) // Invalid public key.
112 changelog.Changes[0].Text = "bad"
113 fetch("", Version{}, 200, pub, nil, ErrChangelogFetch) // Invalid signature.
114 changelog.Changes[0].Text = "test"
115 fetch("", Version{}, 404, pub, nil, ErrChangelogFetch)
116 fetch("", Version{}, 503, pub, nil, ErrChangelogFetch)
117 fetch("", Version{}, 0, pub, nil, ErrChangelogFetch)
118 fetch("bogusurl", Version{}, 200, pub, nil, ErrChangelogFetch)
119
120 check := func(dom string, base Version, baseURL string, status int, pubKey []byte, expVersion Version, expRecord *Record, expChangelog *Changelog, expErr error) {
121 t.Helper()
122
123 mux := &http.ServeMux{}
124 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
125 if status == 0 {
126 panic("bad serve")
127 }
128 w.WriteHeader(status)
129 err := json.NewEncoder(w).Encode(changelog)
130 if err != nil {
131 t.Fatalf("encode changelog: %v", err)
132 }
133 })
134 s := httptest.NewUnstartedServer(mux)
135 s.Config.ErrorLog = golog.New(io.Discard, "", 0)
136 s.Start()
137 defer s.Close()
138 if baseURL == "" {
139 baseURL = s.URL
140 }
141
142 version, record, changelog, err := Check(context.Background(), log.Logger, resolver, dns.Domain{ASCII: dom}, base, baseURL, pubKey)
143 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
144 t.Fatalf("check: got err %v, expected %v", err, expErr)
145 }
146 if version != expVersion || !reflect.DeepEqual(record, expRecord) || !reflect.DeepEqual(changelog, expChangelog) {
147 t.Fatalf("check: got version %v, record %#v, changelog %v, expected %v %#v %v", version, record, changelog, expVersion, expRecord, expChangelog)
148 }
149 }
150
151 check("mox.example", Version{0, 0, 1}, "", 0, pub, Version{0, 0, 1}, &Record{Version: "UPDATES0", Latest: Version{0, 0, 1}}, nil, nil)
152 check("mox.example", Version{0, 0, 0}, "", 200, pub, Version{0, 0, 1}, &Record{Version: "UPDATES0", Latest: Version{0, 0, 1}}, &changelog, nil)
153 check("mox.example", Version{0, 0, 0}, "", 0, pub, Version{0, 0, 1}, &Record{Version: "UPDATES0", Latest: Version{0, 0, 1}}, nil, ErrChangelogFetch)
154 check("absent.example", Version{0, 0, 1}, "", 200, pub, Version{}, nil, nil, ErrNoRecord)
155}
156