| 1 | package instanceupdates |
| 2 | |
| 3 | import ( |
| 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 | |
| 14 | func 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 | |
| 25 | func 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 | |
| 41 | func 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 | |
| 55 | func 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 | |
| 72 | func 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. |
| 81 | func 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 | |
| 99 | func 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 | |
| 117 | func 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 | |
| 138 | type fakeReg struct { |
| 139 | rt *registry.Route |
| 140 | err error |
| 141 | } |
| 142 | |
| 143 | func (f fakeReg) Get(string) (*registry.Route, error) { return f.rt, f.err } |
| 144 | |
| 145 | func TestRegistryReaderFillsFromTheRoute(t *testing.T) { |
| 146 | rt := ®istry.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 "". |
| 166 | func TestRegistryReaderReadsAnEmptyColumnAsAuto(t *testing.T) { |
| 167 | rt := ®istry.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. |
| 183 | func TestRegistryReaderChannelPrefersTheColumn(t *testing.T) { |
| 184 | rep, err := RegistryReader(fakeReg{rt: ®istry.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: ®istry.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. |
| 198 | func 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: ®istry.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 | |