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 "net/http"
5 "net/url"
6 "reflect"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/registry"
12 "github.com/carlosframework/platform/internal/status"
13)
14
15func TestInstanceGroupingPaginatesWithoutLoss(t *testing.T) {
16 rows := []InstanceRow{
17 {Host: "a.example.com", Channel: "stable", HealthWord: "Running"},
18 {Host: "b.example.com", Channel: "edge", HealthWord: "Asleep"},
19 {Host: "c.example.com", Channel: "stable", HealthWord: "Running"},
20 {Host: "d.example.com", Channel: "edge", HealthWord: "Running"},
21 }
22 q := instanceQuery{Group: true}
23 var got []string
24 for i := 0; i < 4; i++ {
25 p := pageInstanceRows(rows, q, 1)
26 for _, r := range p.Rows {
27 got = append(got, r.Host)
28 }
29 if p.NextCursor == "" {
30 break
31 }
32 q.Cursor = p.NextCursor
33 u, err := url.Parse(p.NextURL)
34 if err != nil {
35 t.Fatal(err)
36 }
37 if u.Query().Get("group") != "channel" {
38 t.Fatal(p.NextURL)
39 }
40 }
41 want := []string{"b.example.com", "d.example.com", "a.example.com", "c.example.com"}
42 if !reflect.DeepEqual(got, want) {
43 t.Fatalf("got %v want %v", got, want)
44 }
45 p := pageInstanceRows(rows, instanceQuery{Cursor: q.Cursor}, 50)
46 if len(p.Rows) != 4 {
47 t.Fatal("changing grouping must reset a cursor from the other order")
48 }
49}
50
51func TestInstanceRowsExcludePlatformAliases(t *testing.T) {
52 routes := []RouteView{
53 {Host: "app.example", Account: "ours", Kind: "instance"},
54 {Host: "static.example", Account: "ours", Kind: "static"},
55 {Host: "alias.example", Account: "ours", Kind: "static", Platform: true},
56 {Host: "foreign.example", Account: "theirs", Kind: "instance"},
57 }
58 owned := ownedInstanceRoutes(routes, "ours")
59 if len(owned) != 2 || owned[0].Host != "app.example" || owned[1].Host != "static.example" {
60 t.Fatalf("owned instance routes = %+v", owned)
61 }
62 rows := assembleInstanceRows(&AppView{Routes: routes}, &Account{Sqid: "ours"}, nil)
63 if len(rows) != 2 || rows[0].Host != "app.example" || rows[1].Host != "static.example" {
64 t.Fatalf("instance rows = %+v", rows)
65 }
66}
67
68func TestInstanceChannelFilterAndLinks(t *testing.T) {
69 rows := []InstanceRow{{Host: "a.example.com", Channel: "canary/a+b", HealthWord: "Running"}, {Host: "b.example.com", Channel: "edge", HealthWord: "Asleep"}}
70 q := instanceQuery{Channel: "canary/a+b", Group: true}
71 p := pageInstanceRows(rows, q, 50)
72 if p.Total != 1 || p.Rows[0].Host != "a.example.com" {
73 t.Fatalf("%+v", p)
74 }
75 for _, l := range p.Links {
76 u, e := url.Parse(l.URL)
77 if e != nil {
78 t.Fatal(e)
79 }
80 if u.Query().Get("channel") != q.Channel || u.Query().Get("group") != "channel" {
81 t.Fatal(l.URL)
82 }
83 }
84 q.Channel = "missing"
85 if p := pageInstanceRows(rows, q, 50); p.Total != 0 {
86 t.Fatalf("unmatched channel widened: %+v", p)
87 }
88}
89
90func TestInstanceGroupsKeepTargetSeparate(t *testing.T) {
91 rows := []InstanceRow{{Host: "a.example.com", Channel: "edge", Version: "old"}, {Host: "b.example.com", Channel: "edge", Version: "new"}}
92 groups := groupInstanceRows(rows[:1], []ChannelView{{Name: "edge", Version: "new"}}, rows)
93 if len(groups) != 1 || groups[0].Total != 2 || groups[0].TargetVersion != "new" || groups[0].Rows[0].Version != "old" {
94 t.Fatalf("%+v", groups)
95 }
96}
97
98func TestInstanceGroupCountsRespectActiveFilters(t *testing.T) {
99 rows := []InstanceRow{
100 {Host: "a.example.com", Channel: "edge", HealthWord: "Running"},
101 {Host: "b.example.com", Channel: "edge", HealthWord: "Asleep"},
102 {Host: "c.example.com", Channel: "stable", HealthWord: "Running"},
103 }
104 p := pageInstanceRows(rows, instanceQuery{Group: true, Channel: "edge", Health: "Running"}, 1)
105 groups := groupInstanceRows(p.Rows, nil, p.Matching)
106 if len(groups) != 1 || groups[0].Channel != "edge" || groups[0].Total != 1 || len(groups[0].Rows) != 1 {
107 t.Fatalf("groups = %+v; page = %+v", groups, p)
108 }
109 if p.Counts["Running"] != 2 || p.Counts["Asleep"] != 1 {
110 t.Fatalf("global health counts changed with filters: %+v", p.Counts)
111 }
112}
113
114func TestOwnedInstanceRoutesDoesNotPassOtherAccountsToSelfReport(t *testing.T) {
115 routes := []RouteView{{Host: "mine.example.com", Account: "bab"}, {Host: "theirs.example.com", Account: "bac"}}
116 got := ownedInstanceRoutes(routes, "bab")
117 if len(got) != 1 || got[0].Host != "mine.example.com" {
118 t.Fatalf("%+v", got)
119 }
120}
121
122func TestInstanceDetailOwnsRouteAndAliases(t *testing.T) {
123 fx := newConsoleFixture(t)
124 c := signInPaul(t, fx)
125 reg, err := registry.Open(fx.regPath)
126 if err != nil {
127 t.Fatal(err)
128 }
129 t.Cleanup(func() { reg.Close() })
130 if err := reg.Upsert(registry.Route{Host: "foreign.example.com", App: "kass", Account: "bac", Kind: "instance", Version: "foreign-version"}); err != nil {
131 t.Fatal(err)
132 }
133 if err := reg.UpsertAlias(registry.RouteAlias{Alias: "alias.example.com", Host: "kass.carlosapp.dev", Account: fx.sqid, App: "kass"}); err != nil {
134 t.Fatal(err)
135 }
136 if err := reg.UpsertAlias(registry.RouteAlias{Alias: "foreign-alias.example.com", Host: "kass.carlosapp.dev", Account: "bac", App: "kass"}); err != nil {
137 t.Fatal(err)
138 }
139 status, _, body := get(t, c, fx.u("/apps/kass/instances/kass.carlosapp.dev?channel=stable&group=channel"))
140 if status != http.StatusOK {
141 t.Fatalf("%d: %s", status, body)
142 }
143 for _, want := range []string{"kass.carlosapp.dev", "alias.example.com", `/instances?channel=stable&amp;group=channel`, `href="https://kass.carlosapp.dev"`} {
144 if !strings.Contains(body, want) {
145 t.Errorf("missing %q", want)
146 }
147 }
148 for _, bad := range []string{"foreign-version", "foreign-alias.example.com", "stamp its unit"} {
149 if strings.Contains(body, bad) {
150 t.Errorf("leaked %q", bad)
151 }
152 }
153 for _, host := range []string{"foreign.example.com", "otherapp.carlosapp.dev", "missing.example.com"} {
154 status, _, body := get(t, c, fx.u("/apps/kass/instances/"+host))
155 if status != http.StatusNotFound || body != "404 page not found\n" {
156 t.Fatalf("%s = %d %q", host, status, body)
157 }
158 }
159}
160
161func TestInstanceDetailRejectsPlatformAlias(t *testing.T) {
162 fx := newConsoleFixture(t)
163 c := signInPaul(t, fx)
164 fx.addRoute(t, registry.Route{Host: "platform-alias.example.com", App: "kass", Account: fx.sqid, Kind: "static", Platform: true})
165
166 status, _, body := get(t, c, fx.u("/apps/kass/instances/platform-alias.example.com"))
167 if status != http.StatusNotFound || body != "404 page not found\n" {
168 t.Fatalf("platform alias detail = %d %q", status, body)
169 }
170}
171
172func TestInstanceListLinksToDetailAndKeepsHealthShort(t *testing.T) {
173 fx := newConsoleFixture(t)
174 c := signInPaul(t, fx)
175 status, _, body := get(t, c, fx.u("/apps/kass/instances?group=channel&channel=stable"))
176 if status != http.StatusOK {
177 t.Fatalf("%d %s", status, body)
178 }
179 if !strings.Contains(body, `/instances/kass.carlosapp.dev?channel=stable&amp;group=channel`) {
180 t.Fatal("missing filtered detail link")
181 }
182 if !strings.Contains(body, `href="https://kass.carlosapp.dev"`) {
183 t.Fatal("missing separate external link")
184 }
185 if strings.Contains(body, "stamp its unit") {
186 t.Fatal("health cell still carries infrastructure instructions")
187 }
188 if !strings.Contains(body, `class="instance-group"`) {
189 t.Fatal("missing grouped table")
190 }
191}
192
193func TestInstanceListKeepsUnknownSelectedChannelVisible(t *testing.T) {
194 fx := newConsoleFixture(t)
195 c := signInPaul(t, fx)
196 status, _, body := get(t, c, fx.u("/apps/kass/instances?channel=retired"))
197 if status != http.StatusOK {
198 t.Fatalf("%d %s", status, body)
199 }
200 if !strings.Contains(body, `<option value="retired" selected>retired</option>`) {
201 t.Fatalf("selected unknown channel is invisible: %s", body)
202 }
203 if strings.Contains(body, `<option value="" selected>`) {
204 t.Fatal("unknown channel made the selector appear unfiltered")
205 }
206}
207
208func TestInstanceDetailRendersFlashBeforeNavigation(t *testing.T) {
209 fx := newConsoleFixture(t)
210 c := signInPaul(t, fx)
211 u, err := url.Parse(fx.u("/apps/kass/instances/kass.carlosapp.dev"))
212 if err != nil {
213 t.Fatal(err)
214 }
215 c.Jar.SetCookies(u, []*http.Cookie{{Name: flashCookie, Value: url.QueryEscape("err|flash marker"), Path: "/"}})
216 status, _, body := get(t, c, u.String())
217 if status != http.StatusOK {
218 t.Fatalf("%d %s", status, body)
219 }
220 flash := strings.Index(body, `<p class="banner error">flash marker</p>`)
221 nav := strings.Index(body, `<nav class="tabs"`)
222 if flash < 0 || nav < 0 || flash > nav {
223 t.Fatalf("flash=%d nav=%d", flash, nav)
224 }
225}
226
227// TestInstanceDetailShowsLayoutWord pins R10: the instance page's Layout
228// fact shows the WORD a box's report gives this host, never the reason —
229// proven here with a foreign row, whose reason names paths a member has no
230// business seeing.
231func TestInstanceDetailShowsLayoutWord(t *testing.T) {
232 fx := newConsoleFixture(t)
233 c := signInPaul(t, fx)
234 seedLayoutBox(t, fx.rel, "box-a", time.Now(), &status.LayoutReport{
235 Mode: "nested",
236 Rows: map[string]status.RowLayout{
237 "kass.carlosapp.dev": {Layout: "foreign", Reason: "db \"/secret/path\" is not the flat or nested default"},
238 },
239 })
240 httpStatus, _, body := get(t, c, fx.u("/apps/kass/instances/kass.carlosapp.dev"))
241 if httpStatus != http.StatusOK {
242 t.Fatalf("%d: %s", httpStatus, body)
243 }
244 if !strings.Contains(body, "<dt>Layout</dt><dd>foreign</dd>") {
245 t.Errorf("missing the layout fact showing the word: %s", body)
246 }
247 if strings.Contains(body, "/secret/path") {
248 t.Errorf("the reason leaked onto the page: %s", body)
249 }
250}
251
252// TestInstanceDetailShowsLayoutUnknownWithNoReport: a box that has never
253// reported this host's layout (predates layout reporting, or has armed no
254// pass) must not silently omit the fact — the approved fallback text says
255// so, byte for byte.
256func TestInstanceDetailShowsLayoutUnknownWithNoReport(t *testing.T) {
257 fx := newConsoleFixture(t)
258 c := signInPaul(t, fx)
259 httpStatus, _, body := get(t, c, fx.u("/apps/kass/instances/kass.carlosapp.dev"))
260 if httpStatus != http.StatusOK {
261 t.Fatalf("%d: %s", httpStatus, body)
262 }
263 if !strings.Contains(body, "<dt>Layout</dt><dd>not reported yet</dd>") {
264 t.Errorf("missing the unknown-layout fallback: %s", body)
265 }
266}
267
268// The nil-Releases shape is not separately tested here: appPage's own
269// Assemble already dereferences cfg.Releases (assembleChannels, view.go)
270// before handleAppInstance's layout read ever runs, so the whole instance
271// page panics on that configuration regardless of this feature — a
272// pre-existing gap in Assemble (R10: "do not touch"), not one this task
273// introduces. The `s.cfg.Releases != nil` guard above is still correct
274// defensive code for the day Assemble stops assuming a bucket.
275