carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "strings"
9 "sync/atomic"
10 "testing"
11 "time"
12)
13
14// instanceApp is a fake app serving the /.carlos/instances contract. Its
15// httptest host doubles as the route host (the fetcher is pointed at scheme
16// "http" in tests).
17type instanceApp struct {
18 srv *httptest.Server
19 hits atomic.Int64
20 body string
21 code int
22 wait time.Duration
23}
24
25func newInstanceApp(t *testing.T, body string) *instanceApp {
26 t.Helper()
27 a := &instanceApp{body: body, code: http.StatusOK}
28 a.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29 if r.URL.Path != InstancesPath {
30 http.NotFound(w, r)
31 return
32 }
33 a.hits.Add(1)
34 if a.wait > 0 {
35 time.Sleep(a.wait)
36 }
37 w.WriteHeader(a.code)
38 w.Write([]byte(a.body))
39 }))
40 t.Cleanup(a.srv.Close)
41 return a
42}
43
44func (a *instanceApp) host(t *testing.T) string {
45 t.Helper()
46 u, err := url.Parse(a.srv.URL)
47 if err != nil {
48 t.Fatal(err)
49 }
50 return u.Host
51}
52
53const kassReport = `[
54 {"slug":"riders-2026","status":"ready","region":"eu-west-1"},
55 {"slug":"spring-camp","status":"ready","region":"eu-west-1"},
56 {"slug":"trial-run","status":"archived","detail":"read-only since June"}
57]`
58
59func readyRoute(host string) RouteView {
60 return RouteView{Host: host, Kind: "instance", Status: "ready"}
61}
62
63func TestInstancesFetchAndCache(t *testing.T) {
64 app := newInstanceApp(t, kassReport)
65 f := newInstanceFetcher(nil, "http")
66 rep := f.Report(context.Background(), []RouteView{readyRoute(app.host(t))})
67 if rep == nil || !rep.Reporting {
68 t.Fatalf("report = %+v, want reporting", rep)
69 }
70 if len(rep.Instances) != 3 {
71 t.Fatalf("instances = %+v, want 3", rep.Instances)
72 }
73 if in := rep.Instances[0]; in.Slug != "riders-2026" || in.Status != "ready" || in.Region != "eu-west-1" {
74 t.Fatalf("first instance = %+v", in)
75 }
76 if in := rep.Instances[2]; in.Detail != "read-only since June" {
77 t.Fatalf("third instance = %+v", in)
78 }
79 // A second report within the TTL is served from cache — one hit total.
80 f.Report(context.Background(), []RouteView{readyRoute(app.host(t))})
81 if n := app.hits.Load(); n != 1 {
82 t.Fatalf("app got %d hits, want 1 (cached)", n)
83 }
84}
85
86func TestInstancesNotReporting(t *testing.T) {
87 for name, app := range map[string]*instanceApp{
88 "404": func() *instanceApp { a := newInstanceApp(t, "nope"); a.code = 404; return a }(),
89 "junk": newInstanceApp(t, "not json"),
90 "nonarray": newInstanceApp(t, `{"slug":"x"}`),
91 "oversized": newInstanceApp(t, `[{"slug":"`+strings.Repeat("x", instancesMaxBody)+`"}]`),
92 } {
93 f := newInstanceFetcher(nil, "http")
94 rep := f.Report(context.Background(), []RouteView{readyRoute(app.host(t))})
95 if rep == nil || rep.Reporting {
96 t.Fatalf("%s: report = %+v, want polled but not reporting", name, rep)
97 }
98 }
99 // A host that isn't even listening: same graceful silence.
100 f := newInstanceFetcher(nil, "http")
101 rep := f.Report(context.Background(), []RouteView{readyRoute("127.0.0.1:1")})
102 if rep == nil || rep.Reporting {
103 t.Fatalf("dead host: report = %+v, want not reporting", rep)
104 }
105 // And the failure is cached too — no re-poll inside the TTL.
106 app := newInstanceApp(t, "junk")
107 f2 := newInstanceFetcher(nil, "http")
108 f2.Report(context.Background(), []RouteView{readyRoute(app.host(t))})
109 f2.Report(context.Background(), []RouteView{readyRoute(app.host(t))})
110 if n := app.hits.Load(); n != 1 {
111 t.Fatalf("failing app got %d hits, want 1 (failure cached)", n)
112 }
113}
114
115func TestInstancesSlugRequired(t *testing.T) {
116 app := newInstanceApp(t, `[{"slug":"good"},{"status":"ready"},{"slug":" "}]`)
117 f := newInstanceFetcher(nil, "http")
118 rep := f.Report(context.Background(), []RouteView{readyRoute(app.host(t))})
119 if !rep.Reporting || len(rep.Instances) != 1 || rep.Instances[0].Slug != "good" {
120 t.Fatalf("report = %+v, want only the slugged row", rep)
121 }
122}
123
124func TestInstancesTimeoutDoesNotHang(t *testing.T) {
125 app := newInstanceApp(t, kassReport)
126 app.wait = 500 * time.Millisecond
127 f := newInstanceFetcher(nil, "http")
128 f.timeout = 50 * time.Millisecond
129 start := time.Now()
130 rep := f.Report(context.Background(), []RouteView{readyRoute(app.host(t))})
131 if rep == nil || rep.Reporting {
132 t.Fatalf("slow app report = %+v, want not reporting", rep)
133 }
134 if e := time.Since(start); e > 300*time.Millisecond {
135 t.Fatalf("report took %v — the page hung on a slow app", e)
136 }
137}
138
139func TestInstancesNeverPollsHibernatableOrStatic(t *testing.T) {
140 app := newInstanceApp(t, kassReport)
141 f := newInstanceFetcher(nil, "http")
142 routes := []RouteView{
143 {Host: app.host(t), Kind: "instance", Status: "ready", Hibernate: true},
144 {Host: app.host(t), Kind: "static", Status: "ready"},
145 {Host: app.host(t), Kind: "instance", Status: "pending"},
146 }
147 if rep := f.Report(context.Background(), routes); rep != nil {
148 t.Fatalf("report = %+v, want nil (nothing pollable)", rep)
149 }
150 if n := app.hits.Load(); n != 0 {
151 t.Fatalf("app got %d hits — a hibernatable/static/unready host was polled", n)
152 }
153 // The first pollable route wins; later routes are left alone.
154 other := newInstanceApp(t, `[]`)
155 routes = append(routes, readyRoute(app.host(t)), readyRoute(other.host(t)))
156 rep := f.Report(context.Background(), routes)
157 if rep == nil || rep.Host != app.host(t) {
158 t.Fatalf("report = %+v, want the first ready host", rep)
159 }
160 if n := other.hits.Load(); n != 0 {
161 t.Fatalf("second host got %d hits, want 0", n)
162 }
163}
164
165// TestInstancesNeverPollsSidecar: a sidecar row shares its app's routes
166// slice (Assemble groups by app NAME, no kind filter) but its Host is a
167// derived .sidecar.internal label nothing can dial — polling it wastes the
168// timeout and, worse, makes a HEALTHY app read "not reporting" because the
169// one route the picker chose was never routable to begin with. A ready,
170// non-hibernating, non-static sidecar must still be skipped in favor of the
171// next real instance route.
172func TestInstancesNeverPollsSidecar(t *testing.T) {
173 sidecar := newInstanceApp(t, kassReport)
174 real := newInstanceApp(t, kassReport)
175 f := newInstanceFetcher(nil, "http")
176 routes := []RouteView{
177 {Host: sidecar.host(t), Kind: "sidecar", Status: "ready"},
178 readyRoute(real.host(t)),
179 }
180 rep := f.Report(context.Background(), routes)
181 if rep == nil || rep.Host != real.host(t) {
182 t.Fatalf("report = %+v, want the real instance host — the sidecar is unroutable", rep)
183 }
184 if n := sidecar.hits.Load(); n != 0 {
185 t.Fatalf("sidecar got %d hits — it must never be polled", n)
186 }
187
188 // A sidecar-only route set: nothing pollable, same as hibernating/static.
189 f2 := newInstanceFetcher(nil, "http")
190 if rep := f2.Report(context.Background(), []RouteView{{Host: sidecar.host(t), Kind: "sidecar", Status: "ready"}}); rep != nil {
191 t.Fatalf("report = %+v, want nil — a sidecar-only app has nothing pollable", rep)
192 }
193}
194