carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package instanceupdates
2
3import (
4 "encoding/json"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/registry"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14func serve(t *testing.T, token string) *httptest.Server {
15 t.Helper()
16 mux := http.NewServeMux()
17 Mount(mux, "a.example.com", func() (string, error) { return token, nil }, func(host string) (Report, error) {
18 return Report{Host: host, Settings: release.UpdateSettings{Updates: release.UpdatesManual}, MayChange: []string{"pin"}, Installed: "v1"}, nil
19 })
20 srv := httptest.NewServer(mux)
21 t.Cleanup(srv.Close)
22 return srv
23}
24
25func get(t *testing.T, url, bearer string) (*http.Response, Report) {
26 t.Helper()
27 req, _ := http.NewRequest(http.MethodGet, url, nil)
28 if bearer != "" {
29 req.Header.Set("Authorization", "Bearer "+bearer)
30 }
31 res, err := http.DefaultClient.Do(req)
32 if err != nil {
33 t.Fatal(err)
34 }
35 defer res.Body.Close()
36 var rep Report
37 _ = json.NewDecoder(res.Body).Decode(&rep)
38 return res, rep
39}
40
41func TestUpdatesNeedsTheBearer(t *testing.T) {
42 srv := serve(t, "secret")
43 if res, _ := get(t, srv.URL+"/updates", ""); res.StatusCode != http.StatusUnauthorized {
44 t.Fatalf("no bearer: %d", res.StatusCode)
45 }
46 if res, _ := get(t, srv.URL+"/updates", "wrong"); res.StatusCode != http.StatusUnauthorized {
47 t.Fatalf("wrong bearer: %d", res.StatusCode)
48 }
49 res, rep := get(t, srv.URL+"/updates", "secret")
50 if res.StatusCode != http.StatusOK || rep.Host != "a.example.com" || rep.Settings.Updates != release.UpdatesManual {
51 t.Fatalf("GET: %d %+v", res.StatusCode, rep)
52 }
53}
54
55func TestUpdatesIsReadOnlyAndFixedHost(t *testing.T) {
56 srv := serve(t, "secret")
57 req, _ := http.NewRequest(http.MethodPut, srv.URL+"/updates", nil)
58 req.Header.Set("Authorization", "Bearer secret")
59 res, err := http.DefaultClient.Do(req)
60 if err != nil {
61 t.Fatal(err)
62 }
63 res.Body.Close()
64 if res.StatusCode != http.StatusMethodNotAllowed {
65 t.Fatalf("PUT: %d, want 405", res.StatusCode)
66 }
67 if _, rep := get(t, srv.URL+"/updates?host=other.example.com", "secret"); rep.Host != "a.example.com" {
68 t.Fatal("the host came from the request")
69 }
70}
71
72func TestAnEmptyTokenRefusesEverything(t *testing.T) {
73 srv := serve(t, "")
74 if res, _ := get(t, srv.URL+"/updates", ""); res.StatusCode != http.StatusUnauthorized {
75 t.Fatalf("empty token: %d", res.StatusCode)
76 }
77}
78
79// The token is read on every call: a rewritten env file takes effect on the
80// next request with no restart.
81func TestTheTokenIsReadOnEveryCall(t *testing.T) {
82 cur := "one"
83 mux := http.NewServeMux()
84 Mount(mux, "a.example.com", func() (string, error) { return cur, nil }, func(string) (Report, error) { return Report{}, nil })
85 srv := httptest.NewServer(mux)
86 t.Cleanup(srv.Close)
87 if res, _ := get(t, srv.URL+"/updates", "one"); res.StatusCode != http.StatusOK {
88 t.Fatalf("first token: %d", res.StatusCode)
89 }
90 cur = "two"
91 if res, _ := get(t, srv.URL+"/updates", "one"); res.StatusCode != http.StatusUnauthorized {
92 t.Fatalf("old token after rewrite: %d", res.StatusCode)
93 }
94 if res, _ := get(t, srv.URL+"/updates", "two"); res.StatusCode != http.StatusOK {
95 t.Fatalf("new token: %d", res.StatusCode)
96 }
97}
98
99func TestATokenErrorOrAReaderErrorIsRefused(t *testing.T) {
100 mux := http.NewServeMux()
101 Mount(mux, "a.example.com", func() (string, error) { return "", errors.New("no env") }, func(string) (Report, error) { return Report{}, nil })
102 srv := httptest.NewServer(mux)
103 t.Cleanup(srv.Close)
104 if res, _ := get(t, srv.URL+"/updates", "x"); res.StatusCode != http.StatusUnauthorized {
105 t.Fatalf("token error: %d", res.StatusCode)
106 }
107
108 mux2 := http.NewServeMux()
109 Mount(mux2, "a.example.com", func() (string, error) { return "s", nil }, func(string) (Report, error) { return Report{}, errors.New("boom") })
110 srv2 := httptest.NewServer(mux2)
111 t.Cleanup(srv2.Close)
112 if res, _ := get(t, srv2.URL+"/updates", "s"); res.StatusCode != http.StatusServiceUnavailable {
113 t.Fatalf("reader error: %d, want 503", res.StatusCode)
114 }
115}
116
117func TestMayChangeIsAnArrayNeverNull(t *testing.T) {
118 mux := http.NewServeMux()
119 Mount(mux, "a.example.com", func() (string, error) { return "s", nil }, func(string) (Report, error) { return Report{}, nil })
120 srv := httptest.NewServer(mux)
121 t.Cleanup(srv.Close)
122 req, _ := http.NewRequest(http.MethodGet, srv.URL+"/updates", nil)
123 req.Header.Set("Authorization", "Bearer s")
124 res, err := http.DefaultClient.Do(req)
125 if err != nil {
126 t.Fatal(err)
127 }
128 defer res.Body.Close()
129 var raw map[string]json.RawMessage
130 if err := json.NewDecoder(res.Body).Decode(&raw); err != nil {
131 t.Fatal(err)
132 }
133 if string(raw["may_change"]) != "[]" {
134 t.Fatalf("may_change = %s, want []", raw["may_change"])
135 }
136}
137
138type fakeReg struct {
139 rt *registry.Route
140 err error
141}
142
143func (f fakeReg) Get(string) (*registry.Route, error) { return f.rt, f.err }
144
145func TestRegistryReaderFillsFromTheRoute(t *testing.T) {
146 rt := &registry.Route{
147 Host: "a.example.com", Version: "v1.2.0", ServingVersion: "v1.1.0",
148 UpdateSettings: `{"updates":"manual","channel":"stable","pin":"v1.1.0","control":["pin"]}`,
149 }
150 rep, err := RegistryReader(fakeReg{rt: rt})("a.example.com")
151 if err != nil {
152 t.Fatal(err)
153 }
154 if rep.Installed != "v1.2.0" || rep.Serving != "v1.1.0" || rep.Settings.Updates != release.UpdatesManual ||
155 rep.Settings.Pin != "v1.1.0" || len(rep.MayChange) != 1 || rep.MayChange[0] != "pin" {
156 t.Fatalf("report: %+v", rep)
157 }
158 if rep.Desired != "" || rep.Reason != "" {
159 t.Fatalf("desired/reason must stay empty until plan 4: %+v", rep)
160 }
161}
162
163// An empty column is an instance with default settings (Column() writes ""
164// for them), and its channel is the route's: the report says which channel
165// the instance follows, and the stored column stays "".
166func TestRegistryReaderReadsAnEmptyColumnAsAuto(t *testing.T) {
167 rt := &registry.Route{Host: "a.example.com", Version: "v1", Channel: "beta"}
168 rep, err := RegistryReader(fakeReg{rt: rt})("a.example.com")
169 if err != nil || rep.Settings.Updates != release.UpdatesAuto || rep.Installed != "v1" {
170 t.Fatalf("%+v, %v", rep, err)
171 }
172 if rep.Settings.Channel != "beta" {
173 t.Fatalf("channel of an empty column = %q, want the route's %q", rep.Settings.Channel, "beta")
174 }
175 if rt.UpdateSettings != "" {
176 t.Fatalf("the reader wrote the column: %q", rt.UpdateSettings)
177 }
178}
179
180// A set column is reconcile's effective copy and names its own channel, so
181// it wins over the route's; a set column with no channel falls back to the
182// route's.
183func TestRegistryReaderChannelPrefersTheColumn(t *testing.T) {
184 rep, err := RegistryReader(fakeReg{rt: &registry.Route{Host: "a.example.com", Channel: "stable",
185 UpdateSettings: `{"updates":"manual","channel":"beta"}`}})("a.example.com")
186 if err != nil || rep.Settings.Channel != "beta" {
187 t.Fatalf("column beta, route stable: %+v, %v", rep, err)
188 }
189 rep, err = RegistryReader(fakeReg{rt: &registry.Route{Host: "a.example.com", Channel: "stable",
190 UpdateSettings: `{"updates":"manual"}`}})("a.example.com")
191 if err != nil || rep.Settings.Channel != "stable" {
192 t.Fatalf("column with no channel, route stable: %+v, %v", rep, err)
193 }
194}
195
196// A host the registry does not know, or cannot read, is an error: the verb
197// answers 503, never a 200 with an empty report.
198func TestRegistryReaderErrorsForNoRouteOrAReadError(t *testing.T) {
199 if _, err := RegistryReader(fakeReg{})("gone.example.com"); err == nil {
200 t.Fatal("nil route, nil error: want an error")
201 }
202 if _, err := RegistryReader(fakeReg{err: errors.New("db")})("a.example.com"); err == nil {
203 t.Fatal("read error: want an error")
204 }
205 if _, err := RegistryReader(fakeReg{rt: &registry.Route{UpdateSettings: `{"updates":"bogus"}`}})("a.example.com"); err == nil {
206 t.Fatal("corrupt column: want an error")
207 }
208
209 mux := http.NewServeMux()
210 Mount(mux, "gone.example.com", func() (string, error) { return "s", nil }, RegistryReader(fakeReg{}))
211 srv := httptest.NewServer(mux)
212 t.Cleanup(srv.Close)
213 if res, _ := get(t, srv.URL+"/updates", "s"); res.StatusCode != http.StatusServiceUnavailable {
214 t.Fatalf("unknown host over the wire: %d, want 503", res.StatusCode)
215 }
216}
217