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 "fmt"
5 "strings"
6 "testing"
7
8 "github.com/carlosframework/platform/internal/infraview"
9)
10
11// The same fleet must draw the same picture every time. A diagram that
12// reshuffles between refreshes cannot be read for change, which is most of
13// what an operator opens it for.
14func TestLayoutIsDeterministic(t *testing.T) {
15 a := renderBoth(BuildArch(archFixture()))
16 b := renderBoth(BuildArch(archFixture()))
17 if a != b {
18 t.Fatal("two builds of the same input produced different SVG")
19 }
20}
21
22// renderBoth is the whole page's drawing: every band of the CARLOS estate
23// and every band of the account's other one, in page order. Determinism is a
24// property of the PAGE, so every test about it renders all of it — a split
25// that made one picture stable by letting another reshuffle would be no
26// improvement on one that reshuffled. It also covers the band ORDER, which
27// is part of what a reader would see move.
28func renderBoth(v ArchView) string {
29 return renderEstate(v.CARLOS) + "\n" + renderEstate(v.Rest)
30}
31
32// renderEstate is one estate's drawing: every band of it, in page order.
33func renderEstate(e ArchEstate) string {
34 var out []string
35 for _, b := range e.Bands {
36 out = append(out, b.Label+"\n"+string(RenderArchSVG(b)))
37 }
38 return strings.Join(out, "\n")
39}
40
41// eachBand names every picture on the page — one per estate per region — so
42// a test that must hold of all of them says which one failed. It is the
43// unit those tests want: a band is what gets laid out, sized and drawn, and
44// the properties they assert (nothing overlaps, nothing sits on an edge,
45// the viewBox is positive) are properties of one picture.
46func eachBand(v ArchView) map[string]ArchBand {
47 out := map[string]ArchBand{}
48 for name, e := range map[string]ArchEstate{"CARLOS": v.CARLOS, "rest of the account": v.Rest} {
49 for _, b := range e.Bands {
50 out[name+" / "+b.Label] = b
51 }
52 }
53 return out
54}
55
56// Layer order is the story one band tells: a DNS name enters at the left,
57// resolves to an address, and reaches the box that answers for it. Four
58// columns, because four is what fits the page (archLayer). A record
59// pointing outside the estate must sit to the RIGHT of the record that
60// names it — the diagram's headline unjoined case reads left-to-right like
61// everything else.
62func TestLayoutOrdersLayers(t *testing.T) {
63 in := archFixture()
64 in.Inv.Resources = append(in.Inv.Resources, infraview.Resource{
65 Kind: infraview.KindDNSRecord, ID: "r2", Name: "mail.carloku.com",
66 Attrs: map[string]string{"zone": "carloku.com", "type": "A", "value": "203.0.113.9"},
67 })
68 v := BuildArch(in)
69 x := map[string]int{}
70 for _, n := range archNodes(v) {
71 x[n.ID] = n.X
72 }
73 if !(x["record:r1"] < x["ip:eipalloc-1"] && x["ip:eipalloc-1"] < x["box:carlos-eu-west-1"]) {
74 t.Fatalf("layers out of order: %+v", x)
75 }
76 // And the whole band fits the console's 64rem main column, which is the
77 // entire reason the columns were folded down to four. A picture wider
78 // than this is one the operator has to drag.
79 for name, b := range eachBand(v) {
80 if b.Width > 1024 {
81 t.Errorf("%s: band is %d units wide — it must fit the 1024px main column without scrolling sideways", name, b.Width)
82 }
83 }
84 if !(x["record:r2"] < x["external:203.0.113.9"]) {
85 t.Fatalf("external must sit right of the record that points at it: %+v", x)
86 }
87}
88
89// No node may overlap another: the layout is the only thing standing
90// between a growing fleet and an unreadable page.
91func TestLayoutDoesNotOverlap(t *testing.T) {
92 v := BuildArch(archFixture())
93 // Per picture: two nodes in DIFFERENT diagrams sharing a coordinate is
94 // not an overlap, it is two diagrams.
95 for name, est := range eachBand(v) {
96 for i := range est.Nodes {
97 for j := i + 1; j < len(est.Nodes); j++ {
98 a, b := est.Nodes[i], est.Nodes[j]
99 if a.X < b.X+b.W && b.X < a.X+a.W && a.Y < b.Y+b.H && b.Y < a.Y+a.H {
100 t.Fatalf("%s: %s overlaps %s", name, a.ID, b.ID)
101 }
102 }
103 }
104 }
105}
106
107// The SVG must scale without script and must not carry raw user strings
108// unescaped.
109func TestSVGIsSelfContainedAndEscaped(t *testing.T) {
110 in := archFixture()
111 in.Fleet.Boxes[0].Label = `evil"><script>alert(1)</script>`
112 in.Hosts[0].Label = in.Fleet.Boxes[0].Label
113 out := renderBoth(BuildArch(in))
114 if !strings.Contains(out, "viewBox=") {
115 t.Error("SVG must carry a viewBox so it scales with no JS")
116 }
117 if strings.Contains(out, "<script") {
118 t.Error("a box label must never reach the SVG unescaped")
119 }
120}
121
122// A deployment with no boxes and no inventory yet — an ungranted console on
123// day one — must still draw a valid, positive viewBox. This is the FIRST
124// page an operator sees after this ships, not a contrived corner case.
125func TestLayoutEmptyGraphHasValidViewBox(t *testing.T) {
126 v := BuildArch(ArchInput{})
127 for name, est := range eachBand(v) {
128 if len(est.Nodes) != 0 {
129 t.Fatalf("%s: expected to draw no nodes at all, got %d", name, len(est.Nodes))
130 }
131 if est.Width <= 0 || est.Height <= 0 {
132 t.Fatalf("%s: Width/Height must be positive even with zero nodes: %dx%d", name, est.Width, est.Height)
133 }
134 }
135 out := renderBoth(v)
136 if strings.Count(out, "viewBox=") != 2 {
137 t.Error("both SVGs must carry a viewBox even when the graph is empty")
138 }
139 if strings.Contains(out, `viewBox="0 0 -`) {
140 t.Error("viewBox must never carry a negative dimension")
141 }
142}
143
144// The SVG carries an intrinsic size, not a viewBox alone. Without width and
145// height a browser has nothing to size the element by, so it fills its
146// container and scales the drawing down to fit: a full estate is ~1918 units
147// wide against a ~1024px column, which renders 13px node titles at ~7px and
148// never overflows, so the wrapper's scrollbar never appears either. The
149// labels ARE the content of this diagram, so the size attributes are what
150// make it a diagram rather than a texture.
151func TestSVGCarriesIntrinsicSize(t *testing.T) {
152 v := BuildArch(archFixture())
153 out := renderBoth(v)
154 for name, est := range eachBand(v) {
155 if want := fmt.Sprintf(`width="%d" height="%d"`, est.Width, est.Height); !strings.Contains(out, want) {
156 t.Errorf("%s: SVG must carry %s so it renders 1:1 and .archwrap scrolls; got:\n%s", name, want, out)
157 }
158 if !strings.Contains(out, fmt.Sprintf(`viewBox="0 0 %d %d"`, est.Width, est.Height)) {
159 t.Errorf("%s: the viewBox must still match the intrinsic size", name)
160 }
161 }
162 // Deterministic with the attributes as it was without them.
163 if out != renderBoth(BuildArch(archFixture())) {
164 t.Error("two renders of the same input differ")
165 }
166}
167
168// archAlarmFixture is the flagship's real shape for this: one auto-recover
169// alarm per box, plus one alarm naming a machine the estate read did not
170// return.
171func archAlarmFixture() ArchInput {
172 in := archFixture()
173 in.Inv.Resources = append(in.Inv.Resources,
174 infraview.Resource{Kind: infraview.KindAlarm, ID: "a-1", Name: "auto-recover-eu-west-1",
175 Attrs: map[string]string{"target": "i-123", "state": "OK", "metric": "StatusCheckFailed"}},
176 infraview.Resource{Kind: infraview.KindAlarm, ID: "a-2", Name: "auto-recover-mystery",
177 Attrs: map[string]string{"target": "i-999", "state": "ALARM", "metric": "StatusCheckFailed"}},
178 )
179 return in
180}
181
182// An alarm-bearing estate stays clean: the alarms ride the nodes they
183// watch instead of standing in the flow (which is what put them under the
184// IP-to-box edges), and adding them changes neither the overlap-free
185// property nor determinism.
186func TestLayoutAlarmBearingEstateStaysClean(t *testing.T) {
187 v := BuildArch(archAlarmFixture())
188
189 nodes := archNodes(v)
190 for _, n := range nodes {
191 if n.Kind == "alarm" {
192 t.Fatalf("an alarm must not be a node in the flow: %+v", n)
193 }
194 }
195 worn := map[string]int{}
196 for _, n := range nodes {
197 worn[n.ID] = len(n.Alarms)
198 }
199 // One in each picture: the box's alarm is CARLOS, the alarm on a machine
200 // no box reports belongs with the rest of the account.
201 for _, id := range []string{"box:carlos-eu-west-1", "machine:i-999"} {
202 if worn[id] == 0 {
203 t.Errorf("%s should wear the alarm that watches it: %+v", id, nodes)
204 }
205 }
206
207 for name, est := range eachBand(v) {
208 for i := range est.Nodes {
209 for j := i + 1; j < len(est.Nodes); j++ {
210 a, b := est.Nodes[i], est.Nodes[j]
211 if a.X < b.X+b.W && b.X < a.X+a.W && a.Y < b.Y+b.H && b.Y < a.Y+a.H {
212 t.Fatalf("%s: %s overlaps %s", name, a.ID, b.ID)
213 }
214 }
215 }
216 }
217 if renderBoth(v) != renderBoth(BuildArch(archAlarmFixture())) {
218 t.Error("two builds of an alarm-bearing estate produced different SVG")
219 }
220}
221
222// The ordering contract inside bandsFrom: satellites are dropped, THEN the
223// bands are decided, THEN each is laid out, and only then are its dangling
224// edges pruned. Get it wrong and the page carries edges to nodes nobody
225// draws, leaving the invariant to rest entirely on RenderArchSVG's skip.
226// Two halves: the steps compose in that order, and a built view is already
227// clean when it ships.
228func TestPruneRunsAfterLayoutDropsSatellites(t *testing.T) {
229 g := archGraph{
230 Nodes: []Node{
231 {ID: "box:a", Kind: "box", Title: "a"},
232 {ID: "identity:role", Kind: "identity", Title: "carlos-box-role"},
233 },
234 Edges: []Edge{{From: "box:a", To: "identity:role", Kind: "runs"}},
235 }
236 bands := bandsFrom(g, map[string]string{"box:a": "eu-west-1"})
237 if len(bands) != 1 || bands[0].Region != "eu-west-1" {
238 t.Fatalf("bands = %+v, want one eu-west-1 band", bands)
239 }
240 for _, n := range bands[0].Nodes {
241 if n.ID == "identity:role" {
242 t.Fatal("an identity is table-only; it must be dropped before it is drawn")
243 }
244 }
245 if len(bands[0].Edges) != 0 {
246 t.Errorf("edges = %+v, want none — the only edge pointed at a dropped node", bands[0].Edges)
247 }
248
249 // And on a real build carrying both drawn alarms and a partial estate:
250 // pruning the shipped view again must change nothing, because BuildArch
251 // already pruned AFTER laying out.
252 in := archAlarmFixture()
253 in.Inv.Resources = append(in.Inv.Resources, infraview.Resource{
254 Kind: infraview.KindAlarm, ID: "a-gone",
255 Attrs: map[string]string{"target": "i-terminated", "state": "OK"},
256 })
257 built := BuildArch(in)
258 for name, est := range eachBand(built) {
259 before := len(est.Edges)
260 pruneDanglingEdges(&est)
261 if len(est.Edges) != before {
262 t.Errorf("%s: a shipped view still had %d dangling edges in it", name, before-len(est.Edges))
263 }
264 }
265}
266