eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "net/http"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9)
10
11// Major version pinning (update_major + releaseEntry.Semver): minors and
12// patches follow the channel, a head crossing the pinned major is held —
13// halt-and-notify, never a silent auto-upgrade across a breaking boundary.
14
15// seedSemvers stamps semvers onto feed versions for this test and cleans up.
16func seedSemvers(t *testing.T, m map[string]string) {
17 t.Helper()
18 for v, sv := range m {
19 feedSemvers[v] = sv
20 }
21 t.Cleanup(func() {
22 for v := range m {
23 delete(feedSemvers, v)
24 }
25 })
26}
27
28func TestSemverHelpers(t *testing.T) {
29 if n, ok := semverMajor("2.14.3"); !ok || n != 2 {
30 t.Fatalf("semverMajor(2.14.3) = %d, %v", n, ok)
31 }
32 for _, bad := range []string{"", "2", "2.1", "v2.1.3", "2.1.3-rc1", "2.1.3.4", "x.y.z"} {
33 if _, ok := semverMajor(bad); ok {
34 t.Errorf("semverMajor(%q) accepted", bad)
35 }
36 }
37 for _, good := range []string{"", "0", "1", "12"} {
38 if !validMajor(good) {
39 t.Errorf("validMajor(%q) rejected", good)
40 }
41 }
42 for _, bad := range []string{"-1", "1.0", "one", "99999"} {
43 if validMajor(bad) {
44 t.Errorf("validMajor(%q) accepted", bad)
45 }
46 }
47}
48
49func TestValidateManifestSemver(t *testing.T) {
50 ok := entryFor("new1234")
51 ok.Semver = "1.2.3"
52 m := &releaseManifest{Schema: 1, GeneratedAt: time.Now(), Channels: map[string]releaseEntry{"stable": ok}}
53 if err := validateManifest(m); err != nil {
54 t.Fatalf("valid semver rejected: %v", err)
55 }
56 bad := entryFor("new1234")
57 bad.Semver = "not-a-version"
58 m.Channels["stable"] = bad
59 if err := validateManifest(m); err == nil {
60 t.Fatal("garbage semver accepted")
61 }
62}
63
64func TestMajorHoldSemantics(t *testing.T) {
65 e := func(sv string) releaseEntry { return releaseEntry{Version: "abc1234", Semver: sv} }
66 if hold := majorHold("", e("2.0.0")); hold != "" {
67 t.Fatalf("no pin but held: %s", hold)
68 }
69 if hold := majorHold("1", e("1.9.9")); hold != "" {
70 t.Fatalf("within-major held: %s", hold)
71 }
72 if hold := majorHold("2", e("1.0.0")); hold != "" {
73 t.Fatalf("older major held (pull-backs must pass): %s", hold)
74 }
75 if hold := majorHold("1", e("2.0.0")); hold == "" {
76 t.Fatal("crossing major not held")
77 }
78 // Fail closed: a pinned instance never takes an unlabelled release.
79 if hold := majorHold("1", e("")); hold == "" {
80 t.Fatal("semver-less release not held under a major pin")
81 }
82}
83
84func TestUpdatePassMajorPinHoldsCrossing(t *testing.T) {
85 withTempOpt(t)
86 zeroBake(t) // bake has its own tests; this one is about the major gate
87 calls := stubSystemctl(t)
88 shrink := deployHealthTimeout
89 deployHealthTimeout = 2 * time.Second
90 t.Cleanup(func() { deployHealthTimeout = shrink })
91
92 seedSemvers(t, map[string]string{"v2head99": "2.0.0"})
93 _, setChannels := startFeed(t, map[string]string{"v2head99": "v2-binary"})
94 setChannels(map[string]string{"stable": "v2head99"})
95
96 reg, err := openRegistry(filepath.Join(t.TempDir(), "reg.db"))
97 if err != nil {
98 t.Fatal(err)
99 }
100 s1 := startFakeInstance(t, "h1.example.com", http.StatusOK,
101 updatePrefs{AutoUpdate: true, AutoDownload: true, Channel: "stable", Major: "1"}, "")
102 seedInstance(t, reg, "h1.example.com", s1, "old1111")
103
104 if err := updatePass(reg, "lchat@", false, false); err != nil {
105 t.Fatal(err)
106 }
107 if v, _ := liveVersion("h1.example.com"); v != "old1111" {
108 t.Fatalf("v2 installed across a major-1 pin: live = %q", v)
109 }
110 if _, err := os.Stat(versionBinPath("v2head99")); !os.IsNotExist(err) {
111 t.Fatal("crossing release was downloaded")
112 }
113 if len(*calls) != 0 {
114 t.Fatalf("restarts despite the major hold: %v", *calls)
115 }
116}
117
118func TestUpdatePassMajorPinFollowsWithinMajor(t *testing.T) {
119 withTempOpt(t)
120 zeroBake(t)
121 stubSystemctl(t)
122 shrink := deployHealthTimeout
123 deployHealthTimeout = 2 * time.Second
124 t.Cleanup(func() { deployHealthTimeout = shrink })
125
126 seedSemvers(t, map[string]string{"v1patch7": "1.4.2"})
127 _, setChannels := startFeed(t, map[string]string{"v1patch7": "patch-binary"})
128 setChannels(map[string]string{"stable": "v1patch7"})
129
130 reg, err := openRegistry(filepath.Join(t.TempDir(), "reg.db"))
131 if err != nil {
132 t.Fatal(err)
133 }
134 s1 := startFakeInstance(t, "h1.example.com", http.StatusOK,
135 updatePrefs{AutoUpdate: true, AutoDownload: true, Channel: "stable", Major: "1"}, "")
136 seedInstance(t, reg, "h1.example.com", s1, "old1111")
137
138 if err := updatePass(reg, "lchat@", false, false); err != nil {
139 t.Fatal(err)
140 }
141 if v, _ := liveVersion("h1.example.com"); v != "v1patch7" {
142 t.Fatalf("within-major release held: live = %q", v)
143 }
144}
145
146func TestUpdatePassMajorPinHoldsSemverlessHead(t *testing.T) {
147 withTempOpt(t)
148 zeroBake(t)
149 stubSystemctl(t)
150
151 // No seedSemvers: the head carries no semver at all — the pin fails closed.
152 _, setChannels := startFeed(t, map[string]string{"nolabel1": "content"})
153 setChannels(map[string]string{"stable": "nolabel1"})
154
155 reg, err := openRegistry(filepath.Join(t.TempDir(), "reg.db"))
156 if err != nil {
157 t.Fatal(err)
158 }
159 s1 := startFakeInstance(t, "h1.example.com", http.StatusOK,
160 updatePrefs{AutoUpdate: true, AutoDownload: true, Channel: "stable", Major: "1"}, "")
161 seedInstance(t, reg, "h1.example.com", s1, "old1111")
162
163 if err := updatePass(reg, "lchat@", false, false); err != nil {
164 t.Fatal(err)
165 }
166 if v, _ := liveVersion("h1.example.com"); v != "old1111" {
167 t.Fatalf("semver-less head installed under a major pin: live = %q", v)
168 }
169}
170
171func TestUpdatePassMajorPinHoldsHibernating(t *testing.T) {
172 withTempOpt(t)
173 zeroBake(t)
174 calls := stubSystemctl(t)
175
176 oldData := instanceDataDir
177 instanceDataDir = t.TempDir()
178 t.Cleanup(func() { instanceDataDir = oldData })
179
180 seedSemvers(t, map[string]string{"v2head99": "2.0.0"})
181 _, setChannels := startFeed(t, map[string]string{"v2head99": "v2-binary"})
182 setChannels(map[string]string{"stable": "v2head99"})
183
184 reg, err := openRegistry(filepath.Join(t.TempDir(), "reg.db"))
185 if err != nil {
186 t.Fatal(err)
187 }
188 src := filepath.Join(t.TempDir(), "old")
189 writeFakeBinary(t, src, "old-binary")
190 if err := stageVersion(src, "old1111"); err != nil {
191 t.Fatal(err)
192 }
193 if err := pointLive("h1.hib", "old1111"); err != nil {
194 t.Fatal(err)
195 }
196 if err := reg.upsert(Route{Host: "h1.hib", Kind: "instance", Status: "ready",
197 Socket: filepath.Join(t.TempDir(), "no.sock"), Hibernate: true, Version: "old1111"}); err != nil {
198 t.Fatal(err)
199 }
200 db, err := openDB(filepath.Join(instanceDataDir, "h1.hib.db"))
201 if err != nil {
202 t.Fatal(err)
203 }
204 if err := db.setConfig("update_major", "1"); err != nil {
205 t.Fatal(err)
206 }
207 db.sql.Close()
208
209 if err := updatePass(reg, "lchat@", false, false); err != nil {
210 t.Fatal(err)
211 }
212 if v, _ := liveVersion("h1.hib"); v != "old1111" {
213 t.Fatalf("hibernating host repointed across its major pin: live = %q", v)
214 }
215 if len(*calls) != 0 {
216 t.Fatalf("systemctl touched: %v", *calls)
217 }
218}
219