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 "strings"
5 "testing"
6
7 "github.com/carlosframework/platform/internal/infraview"
8)
9
10// TestNoNodeSitsOnAnEdgePath is the invariant the alarm column broke: a
11// node that is not an endpoint of an edge must not sit ON that edge's
12// path. Nodes paint after edges, so a node standing in the middle of a
13// line hides it and the picture reads as a connection that does not
14// exist — an IP appearing to reach its box THROUGH an alarm.
15//
16// An adjacent-layer edge is drawn as three segments (out, across, in), so
17// the check is the two horizontal runs at the endpoints' own y values plus
18// the vertical hop between them. A LAYER-SKIPPING edge is drawn as five,
19// around a lane below every node — see edgeSegments, which mirrors
20// RenderArchSVG exactly. Mirroring matters: a test that modelled the
21// three-segment path for both would be checking geometry the renderer does
22// not emit, and would pass while the picture lied.
23func TestNoNodeSitsOnAnEdgePath(t *testing.T) {
24 // Per picture: a node in the other diagram cannot obscure this one's
25 // lines, and coordinates only mean anything inside one picture.
26 for name, est := range eachBand(BuildArch(archFixture())) {
27 assertNoNodeOnAnEdge(t, name, est)
28 }
29}
30
31// TestLayerSkippingEdgeRoutesAroundTheColumnsItSkips is the case #9 caught in
32// a browser and this file's fixture did not have: an edge that skips a
33// layer, with an unrelated node sitting in the layer between its ends, on
34// the same row.
35//
36// With the plain three-segment route the line turned at the midpoint, which
37// for such an edge is INSIDE the skipped column, so it ran under that node
38// and out the other side. Nodes paint after edges, so the top row read as one
39// continuous chain — `carloku.com -> au.oncarlos.com -> 13.55.0.44 -> 10
40// inbound-smtp… -> carlos-flagship-au -> kass -> bucket` — of which two hops
41// are fiction.
42//
43// #9's own edge was ip -> box past an `external` column; #390 folded
44// external into the address column, so that pair is adjacent now. The skip
45// that remains is record -> box: a record whose value is an address the
46// machine holds directly rather than through an allocated IP (recordTarget),
47// which points at the box from two columns away, past whatever ip or
48// external node shares its row.
49func TestLayerSkippingEdgeRoutesAroundTheColumnsItSkips(t *testing.T) {
50 est := ArchBand{}
51 est.Nodes = []Node{
52 {ID: "record:au", Kind: "record", Title: "au.oncarlos.com"},
53 {ID: "ext:mx", Kind: "external", Title: "10 inbound-smtp.eu-west-1.amazonaws.com"},
54 {ID: "box:au", Kind: "box", Title: "carlos-flagship-au"},
55 }
56 // The one edge, skipping the address column the MX node occupies.
57 est.Edges = []Edge{{From: "record:au", To: "box:au", Kind: "resolves"}}
58 layout(&est)
59 assertNoNodeOnAnEdge(t, "skip", est)
60
61 // And it is the skip routing that is drawn, not the plain one.
62 out := string(RenderArchSVG(est))
63 if !strings.Contains(out, "archedge-skip") {
64 t.Errorf("a layer-skipping edge must be routed around, not straight through:\n%s", out)
65 }
66}
67
68// seg is one axis-aligned run: `at` is the fixed coordinate, a..b the span.
69type seg struct{ at, a, b int }
70
71// edgeSegments is RenderArchSVG's routing, in the same two branches and with
72// the same numbers. Kept beside the renderer's own code deliberately: the
73// value of this whole file is that it checks what is DRAWN.
74func edgeSegments(v ArchBand, from, to Node) (horizontal, vertical []seg) {
75 x1, y1 := from.X+from.W, from.Y+from.H/2
76 x2, y2 := to.X, to.Y+to.H/2
77 if archLayer[to.Kind]-archLayer[from.Kind] > 1 {
78 lane := v.Height - padding/2
79 return []seg{
80 {at: y1, a: x1, b: x1 + gapX/2},
81 {at: lane, a: x1 + gapX/2, b: x2 - gapX/2},
82 {at: y2, a: x2 - gapX/2, b: x2},
83 }, []seg{
84 {at: x1 + gapX/2, a: y1, b: lane},
85 {at: x2 - gapX/2, a: lane, b: y2},
86 }
87 }
88 mid := (x1 + x2) / 2
89 return []seg{{at: y1, a: x1, b: mid}, {at: y2, a: mid, b: x2}},
90 []seg{{at: mid, a: y1, b: y2}}
91}
92
93// assertNoNodeOnAnEdge checks every drawn edge against every node that is not
94// one of its endpoints. name says which picture, since every band is checked
95// separately and their coordinates are unrelated.
96func assertNoNodeOnAnEdge(t *testing.T, name string, v ArchBand) {
97 t.Helper()
98 pos := map[string]Node{}
99 for _, n := range v.Nodes {
100 pos[n.ID] = n
101 }
102 for _, e := range v.Edges {
103 from, to := pos[e.From], pos[e.To]
104 hs, vs := edgeSegments(v, from, to)
105 for _, n := range v.Nodes {
106 if n.ID == e.From || n.ID == e.To {
107 continue
108 }
109 for _, h := range hs {
110 if segmentHitsNode(h.a, h.b, h.at, n) {
111 t.Errorf("%s: edge %s -> %s runs horizontally through node %s (%d,%d %dx%d)",
112 name, e.From, e.To, n.ID, n.X, n.Y, n.W, n.H)
113 }
114 }
115 for _, vv := range vs {
116 if verticalHitsNode(vv.at, vv.a, vv.b, n) {
117 t.Errorf("%s: edge %s -> %s runs vertically through node %s (%d,%d %dx%d)",
118 name, e.From, e.To, n.ID, n.X, n.Y, n.W, n.H)
119 }
120 }
121 }
122 }
123}
124
125func segmentHitsNode(xa, xb, y int, n Node) bool {
126 if xa > xb {
127 xa, xb = xb, xa
128 }
129 return y > n.Y && y < n.Y+n.H && xb > n.X && xa < n.X+n.W
130}
131
132func verticalHitsNode(x, ya, yb int, n Node) bool {
133 if ya > yb {
134 ya, yb = yb, ya
135 }
136 return x > n.X && x < n.X+n.W && yb > n.Y && ya < n.Y+n.H
137}
138
139// TestAlarmsRideTheNodeTheyWatch: an alarm is a property OF the thing it
140// watches, so it is drawn on that node rather than as a node of its own in
141// the flow. A firing alarm must be distinguishable from a healthy one by
142// something other than its wording.
143func TestAlarmsRideTheNodeTheyWatch(t *testing.T) {
144 in := archFixture()
145 in.Inv.Resources = append(in.Inv.Resources,
146 infraview.Resource{Provider: "aws", Kind: infraview.KindAlarm, NativeType: "cloudwatch:alarm",
147 ID: "arn:alarm/ok", Name: "carlos-eu-west-1-auto-recover", Region: "eu-west-1",
148 Attrs: map[string]string{"state": "OK", "metric": "StatusCheckFailed_System", "target": "i-123"}},
149 infraview.Resource{Provider: "aws", Kind: infraview.KindAlarm, NativeType: "cloudwatch:alarm",
150 ID: "arn:alarm/firing", Name: "carlos-eu-west-1-cpu", Region: "eu-west-1",
151 Attrs: map[string]string{"state": "ALARM", "metric": "CPUUtilization", "target": "i-123"}},
152 )
153 v := BuildArch(in)
154
155 if n := nodeByID(v, "alarm:arn:alarm/ok"); n != nil {
156 t.Fatalf("an alarm must not be drawn as its own node in the flow: %+v", n)
157 }
158 box := nodeByID(v, "box:carlos-eu-west-1")
159 if box == nil {
160 t.Fatal("no box node")
161 }
162 if len(box.Alarms) != 2 {
163 t.Fatalf("box carries %d alarms, want 2: %+v", len(box.Alarms), box.Alarms)
164 }
165 var firing, ok bool
166 for _, a := range box.Alarms {
167 if a.Name == "carlos-eu-west-1-cpu" && a.Firing {
168 firing = true
169 }
170 if a.Name == "carlos-eu-west-1-auto-recover" && !a.Firing {
171 ok = true
172 }
173 }
174 if !firing || !ok {
175 t.Errorf("firing state not carried per alarm: %+v", box.Alarms)
176 }
177
178 out := renderBoth(v)
179 if !strings.Contains(out, "archalarm-firing") {
180 t.Error("a firing alarm must render differently from a healthy one")
181 }
182 if !strings.Contains(out, "carlos-eu-west-1-cpu ALARM") {
183 t.Error("the alarm's name and state must be reachable on the node (hover title)")
184 }
185}
186
187// TestSVGTextIsFittedToItsBox: nothing may bleed past the rect it belongs
188// to. The full string stays reachable as the node's hover title, so
189// truncation costs nothing but the overflow.
190func TestSVGTextIsFittedToItsBox(t *testing.T) {
191 in := archFixture()
192 v := BuildArch(in)
193 long := "console, kass, keymail, platform-site, woodstar, status, carloku-site"
194 // A box's apps line is the longest string on the page and the one most
195 // likely to bleed. Written into the BAND's own node slice — Nodes() is a
196 // fresh copy per call, so mutating what it returns would change nothing
197 // and this test would pass by drawing the untouched fixture.
198 touched := false
199 for bi := range v.CARLOS.Bands {
200 for i := range v.CARLOS.Bands[bi].Nodes {
201 if v.CARLOS.Bands[bi].Nodes[i].Kind != "box" {
202 continue
203 }
204 v.CARLOS.Bands[bi].Nodes[i].Extra = []string{
205 long, "static 3 · resident 4 · hibernating 2 · awake 6 · asleep 3",
206 }
207 touched = true
208 }
209 }
210 if !touched {
211 t.Fatal("no box node to widen — the fixture stopped exercising this")
212 }
213 out := renderBoth(v)
214 if strings.Contains(out, ">"+long+"<") {
215 t.Error("a title wider than its rect must be truncated, not drawn past the box")
216 }
217 if !strings.Contains(out, "…") {
218 t.Error("truncated text must show an ellipsis")
219 }
220 if !strings.Contains(out, long) {
221 t.Error("the full string must survive as the node's hover title")
222 }
223}
224
225// TestByoBoxIsNotPriced: the platform does not pay for a box it does not
226// own, so a bring-your-own machine gets no monthly figure and no
227// "priced by the default spec" chip — a price there would be a claim about
228// someone else's bill.
229func TestByoBoxIsNotPriced(t *testing.T) {
230 v := BuildArch(archFixture())
231 for _, c := range v.Boxes {
232 if c.CloudID != "" {
233 continue
234 }
235 if c.USDMonth != 0 {
236 t.Errorf("BYO box %s priced at %v", c.Label, c.USDMonth)
237 }
238 for _, d := range c.Drift {
239 if strings.Contains(d, "default spec") {
240 t.Errorf("BYO box %s carries a pricing-drift chip: %q", c.Label, d)
241 }
242 }
243 }
244}
245