eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "crypto/ed25519"
5 "crypto/rand"
6 "encoding/base64"
7 "encoding/json"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12 "time"
13)
14
15func TestUpdatePinSettings(t *testing.T) {
16 ts, _ := newTestServer(t)
17
18 // A pin must be a plausible version id (it becomes a store dir name).
19 if resp := adminPost(t, ts.URL+"/api/admin/settings", `{"update_pin":"../evil"}`); resp.StatusCode != http.StatusBadRequest {
20 t.Fatalf("bad pin: want 400, got %d", resp.StatusCode)
21 }
22
23 if resp := adminPost(t, ts.URL+"/api/admin/settings", `{"update_pin":"abc1234"}`); resp.StatusCode != http.StatusOK {
24 t.Fatalf("set pin: %d", resp.StatusCode)
25 }
26 var prefs struct {
27 Channel string `json:"channel"`
28 Pin string `json:"pin"`
29 }
30 json.NewDecoder(mustGet(t, ts.URL+"/api/update-prefs").Body).Decode(&prefs)
31 if prefs.Pin != "abc1234" {
32 t.Fatalf("prefs pin = %q", prefs.Pin)
33 }
34 var settings struct {
35 UpdatePin string `json:"update_pin"`
36 }
37 json.NewDecoder(adminGet(t, ts.URL+"/api/admin/settings").Body).Decode(&settings)
38 if settings.UpdatePin != "abc1234" {
39 t.Fatalf("settings pin = %q", settings.UpdatePin)
40 }
41
42 // The admin UI resumes updates by picking a channel — one combined write
43 // that sets the channel and clears the pin.
44 if resp := adminPost(t, ts.URL+"/api/admin/settings", `{"update_channel":"beta","update_pin":""}`); resp.StatusCode != http.StatusOK {
45 t.Fatalf("clear pin: %d", resp.StatusCode)
46 }
47 json.NewDecoder(mustGet(t, ts.URL+"/api/update-prefs").Body).Decode(&prefs)
48 if prefs.Pin != "" || prefs.Channel != "beta" {
49 t.Fatalf("after clear = %+v, want no pin on beta", prefs)
50 }
51}
52
53func TestUpdatePrefsDefaultsAndSettings(t *testing.T) {
54 ts, _ := newTestServer(t)
55
56 // Defaults: auto-update on, auto-download on, stable channel — un-authed,
57 // this is what the box updater reads over the instance socket.
58 var prefs struct {
59 AutoUpdate bool `json:"auto_update"`
60 AutoDownload bool `json:"auto_download"`
61 Channel string `json:"channel"`
62 }
63 json.NewDecoder(mustGet(t, ts.URL+"/api/update-prefs").Body).Decode(&prefs)
64 if !prefs.AutoUpdate || !prefs.AutoDownload || prefs.Channel != "stable" {
65 t.Fatalf("defaults = %+v, want on/on/stable", prefs)
66 }
67
68 // An unknown channel is rejected, and rejected atomically — a combined
69 // request must not half-apply its valid fields before the 400.
70 if resp := adminPost(t, ts.URL+"/api/admin/settings", `{"user_invites":true,"update_channel":"nightly"}`); resp.StatusCode != http.StatusBadRequest {
71 t.Fatalf("bad channel: want 400, got %d", resp.StatusCode)
72 }
73 var check struct {
74 UserInvites bool `json:"user_invites"`
75 }
76 json.NewDecoder(adminGet(t, ts.URL+"/api/admin/settings").Body).Decode(&check)
77 if check.UserInvites {
78 t.Fatal("rejected request still flipped user_invites (partial write)")
79 }
80
81 // Partial update: flip auto-update off + move to edge; download untouched.
82 if resp := adminPost(t, ts.URL+"/api/admin/settings", `{"update_auto":false,"update_channel":"edge"}`); resp.StatusCode != http.StatusOK {
83 t.Fatalf("set: %d", resp.StatusCode)
84 }
85 json.NewDecoder(mustGet(t, ts.URL+"/api/update-prefs").Body).Decode(&prefs)
86 if prefs.AutoUpdate || !prefs.AutoDownload || prefs.Channel != "edge" {
87 t.Fatalf("after set = %+v, want off/on/edge", prefs)
88 }
89
90 // The admin console reads the same values (plus the availability block).
91 var settings struct {
92 UpdateAuto bool `json:"update_auto"`
93 UpdateDownload bool `json:"update_download"`
94 UpdateChannel string `json:"update_channel"`
95 Update map[string]any `json:"update"`
96 }
97 json.NewDecoder(adminGet(t, ts.URL+"/api/admin/settings").Body).Decode(&settings)
98 if settings.UpdateAuto || !settings.UpdateDownload || settings.UpdateChannel != "edge" {
99 t.Fatalf("settings = %+v", settings)
100 }
101 if settings.Update["build"] != buildVersion {
102 t.Fatalf("update.build = %v", settings.Update["build"])
103 }
104 if _, ok := settings.Update["available_version"]; ok {
105 t.Fatal("available_version present before any manifest check")
106 }
107}
108
109func TestUpdateCheckerSurfacesAvailableVersion(t *testing.T) {
110 ts, srv := newTestServer(t)
111
112 // A signed manifest on a local server, trusted via a swapped-in test key.
113 pub, priv, _ := ed25519.GenerateKey(rand.Reader)
114 wrapper, err := signReleaseManifest(priv, &releaseManifest{
115 Channels: map[string]releaseEntry{
116 "stable": {Version: "new1234", URL: "https://example.com/lchat",
117 SHA256: strings.Repeat("ab", 32), Notes: "a better build"},
118 },
119 })
120 if err != nil {
121 t.Fatal(err)
122 }
123 feed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
124 w.Write(wrapper)
125 }))
126 defer feed.Close()
127
128 oldKey, oldURL := releasePubKeyB64, releaseManifestURL
129 releasePubKeyB64 = base64.StdEncoding.EncodeToString(pub)
130 releaseManifestURL = feed.URL
131 defer func() { releasePubKeyB64, releaseManifestURL = oldKey, oldURL }()
132
133 if err := srv.checkForUpdate(); err != nil {
134 t.Fatalf("checkForUpdate: %v", err)
135 }
136
137 var settings struct {
138 Update struct {
139 AvailableVersion string `json:"available_version"`
140 AvailableNotes string `json:"available_notes"`
141 CheckedAt int64 `json:"checked_at"`
142 } `json:"update"`
143 }
144 json.NewDecoder(adminGet(t, ts.URL+"/api/admin/settings").Body).Decode(&settings)
145 if settings.Update.AvailableVersion != "new1234" || settings.Update.AvailableNotes != "a better build" {
146 t.Fatalf("update block = %+v", settings.Update)
147 }
148 if settings.Update.CheckedAt == 0 || time.Since(time.Unix(settings.Update.CheckedAt, 0)) > time.Minute {
149 t.Fatalf("checked_at = %d", settings.Update.CheckedAt)
150 }
151
152 // Switching channels reflects immediately from the cached manifest — and a
153 // channel with no entry (edge here) just shows nothing available.
154 adminPost(t, ts.URL+"/api/admin/settings", `{"update_channel":"edge"}`)
155 var after struct {
156 Update map[string]any `json:"update"`
157 }
158 json.NewDecoder(adminGet(t, ts.URL+"/api/admin/settings").Body).Decode(&after)
159 if _, ok := after.Update["available_version"]; ok {
160 t.Fatal("edge has no entry; available_version should be absent")
161 }
162
163 // A tampered feed must not replace the cached good manifest.
164 wrapper[len(wrapper)-10]++
165 if err := srv.checkForUpdate(); err == nil {
166 t.Fatal("tampered manifest accepted")
167 }
168}
169