package console import ( "strings" "testing" "github.com/carlosframework/platform/internal/infraview" ) // TestNoNodeSitsOnAnEdgePath is the invariant the alarm column broke: a // node that is not an endpoint of an edge must not sit ON that edge's // path. Nodes paint after edges, so a node standing in the middle of a // line hides it and the picture reads as a connection that does not // exist — an IP appearing to reach its box THROUGH an alarm. // // An adjacent-layer edge is drawn as three segments (out, across, in), so // the check is the two horizontal runs at the endpoints' own y values plus // the vertical hop between them. A LAYER-SKIPPING edge is drawn as five, // around a lane below every node — see edgeSegments, which mirrors // RenderArchSVG exactly. Mirroring matters: a test that modelled the // three-segment path for both would be checking geometry the renderer does // not emit, and would pass while the picture lied. func TestNoNodeSitsOnAnEdgePath(t *testing.T) { // Per picture: a node in the other diagram cannot obscure this one's // lines, and coordinates only mean anything inside one picture. for name, est := range eachBand(BuildArch(archFixture())) { assertNoNodeOnAnEdge(t, name, est) } } // TestLayerSkippingEdgeRoutesAroundTheColumnsItSkips is the case #9 caught in // a browser and this file's fixture did not have: an edge that skips a // layer, with an unrelated node sitting in the layer between its ends, on // the same row. // // With the plain three-segment route the line turned at the midpoint, which // for such an edge is INSIDE the skipped column, so it ran under that node // and out the other side. Nodes paint after edges, so the top row read as one // continuous chain — `carloku.com -> au.oncarlos.com -> 13.55.0.44 -> 10 // inbound-smtp… -> carlos-flagship-au -> kass -> bucket` — of which two hops // are fiction. // // #9's own edge was ip -> box past an `external` column; #390 folded // external into the address column, so that pair is adjacent now. The skip // that remains is record -> box: a record whose value is an address the // machine holds directly rather than through an allocated IP (recordTarget), // which points at the box from two columns away, past whatever ip or // external node shares its row. func TestLayerSkippingEdgeRoutesAroundTheColumnsItSkips(t *testing.T) { est := ArchBand{} est.Nodes = []Node{ {ID: "record:au", Kind: "record", Title: "au.oncarlos.com"}, {ID: "ext:mx", Kind: "external", Title: "10 inbound-smtp.eu-west-1.amazonaws.com"}, {ID: "box:au", Kind: "box", Title: "carlos-flagship-au"}, } // The one edge, skipping the address column the MX node occupies. est.Edges = []Edge{{From: "record:au", To: "box:au", Kind: "resolves"}} layout(&est) assertNoNodeOnAnEdge(t, "skip", est) // And it is the skip routing that is drawn, not the plain one. out := string(RenderArchSVG(est)) if !strings.Contains(out, "archedge-skip") { t.Errorf("a layer-skipping edge must be routed around, not straight through:\n%s", out) } } // seg is one axis-aligned run: `at` is the fixed coordinate, a..b the span. type seg struct{ at, a, b int } // edgeSegments is RenderArchSVG's routing, in the same two branches and with // the same numbers. Kept beside the renderer's own code deliberately: the // value of this whole file is that it checks what is DRAWN. func edgeSegments(v ArchBand, from, to Node) (horizontal, vertical []seg) { x1, y1 := from.X+from.W, from.Y+from.H/2 x2, y2 := to.X, to.Y+to.H/2 if archLayer[to.Kind]-archLayer[from.Kind] > 1 { lane := v.Height - padding/2 return []seg{ {at: y1, a: x1, b: x1 + gapX/2}, {at: lane, a: x1 + gapX/2, b: x2 - gapX/2}, {at: y2, a: x2 - gapX/2, b: x2}, }, []seg{ {at: x1 + gapX/2, a: y1, b: lane}, {at: x2 - gapX/2, a: lane, b: y2}, } } mid := (x1 + x2) / 2 return []seg{{at: y1, a: x1, b: mid}, {at: y2, a: mid, b: x2}}, []seg{{at: mid, a: y1, b: y2}} } // assertNoNodeOnAnEdge checks every drawn edge against every node that is not // one of its endpoints. name says which picture, since every band is checked // separately and their coordinates are unrelated. func assertNoNodeOnAnEdge(t *testing.T, name string, v ArchBand) { t.Helper() pos := map[string]Node{} for _, n := range v.Nodes { pos[n.ID] = n } for _, e := range v.Edges { from, to := pos[e.From], pos[e.To] hs, vs := edgeSegments(v, from, to) for _, n := range v.Nodes { if n.ID == e.From || n.ID == e.To { continue } for _, h := range hs { if segmentHitsNode(h.a, h.b, h.at, n) { t.Errorf("%s: edge %s -> %s runs horizontally through node %s (%d,%d %dx%d)", name, e.From, e.To, n.ID, n.X, n.Y, n.W, n.H) } } for _, vv := range vs { if verticalHitsNode(vv.at, vv.a, vv.b, n) { t.Errorf("%s: edge %s -> %s runs vertically through node %s (%d,%d %dx%d)", name, e.From, e.To, n.ID, n.X, n.Y, n.W, n.H) } } } } } func segmentHitsNode(xa, xb, y int, n Node) bool { if xa > xb { xa, xb = xb, xa } return y > n.Y && y < n.Y+n.H && xb > n.X && xa < n.X+n.W } func verticalHitsNode(x, ya, yb int, n Node) bool { if ya > yb { ya, yb = yb, ya } return x > n.X && x < n.X+n.W && yb > n.Y && ya < n.Y+n.H } // TestAlarmsRideTheNodeTheyWatch: an alarm is a property OF the thing it // watches, so it is drawn on that node rather than as a node of its own in // the flow. A firing alarm must be distinguishable from a healthy one by // something other than its wording. func TestAlarmsRideTheNodeTheyWatch(t *testing.T) { in := archFixture() in.Inv.Resources = append(in.Inv.Resources, infraview.Resource{Provider: "aws", Kind: infraview.KindAlarm, NativeType: "cloudwatch:alarm", ID: "arn:alarm/ok", Name: "carlos-eu-west-1-auto-recover", Region: "eu-west-1", Attrs: map[string]string{"state": "OK", "metric": "StatusCheckFailed_System", "target": "i-123"}}, infraview.Resource{Provider: "aws", Kind: infraview.KindAlarm, NativeType: "cloudwatch:alarm", ID: "arn:alarm/firing", Name: "carlos-eu-west-1-cpu", Region: "eu-west-1", Attrs: map[string]string{"state": "ALARM", "metric": "CPUUtilization", "target": "i-123"}}, ) v := BuildArch(in) if n := nodeByID(v, "alarm:arn:alarm/ok"); n != nil { t.Fatalf("an alarm must not be drawn as its own node in the flow: %+v", n) } box := nodeByID(v, "box:carlos-eu-west-1") if box == nil { t.Fatal("no box node") } if len(box.Alarms) != 2 { t.Fatalf("box carries %d alarms, want 2: %+v", len(box.Alarms), box.Alarms) } var firing, ok bool for _, a := range box.Alarms { if a.Name == "carlos-eu-west-1-cpu" && a.Firing { firing = true } if a.Name == "carlos-eu-west-1-auto-recover" && !a.Firing { ok = true } } if !firing || !ok { t.Errorf("firing state not carried per alarm: %+v", box.Alarms) } out := renderBoth(v) if !strings.Contains(out, "archalarm-firing") { t.Error("a firing alarm must render differently from a healthy one") } if !strings.Contains(out, "carlos-eu-west-1-cpu ALARM") { t.Error("the alarm's name and state must be reachable on the node (hover title)") } } // TestSVGTextIsFittedToItsBox: nothing may bleed past the rect it belongs // to. The full string stays reachable as the node's hover title, so // truncation costs nothing but the overflow. func TestSVGTextIsFittedToItsBox(t *testing.T) { in := archFixture() v := BuildArch(in) long := "console, kass, keymail, platform-site, woodstar, status, carloku-site" // A box's apps line is the longest string on the page and the one most // likely to bleed. Written into the BAND's own node slice — Nodes() is a // fresh copy per call, so mutating what it returns would change nothing // and this test would pass by drawing the untouched fixture. touched := false for bi := range v.CARLOS.Bands { for i := range v.CARLOS.Bands[bi].Nodes { if v.CARLOS.Bands[bi].Nodes[i].Kind != "box" { continue } v.CARLOS.Bands[bi].Nodes[i].Extra = []string{ long, "static 3 · resident 4 · hibernating 2 · awake 6 · asleep 3", } touched = true } } if !touched { t.Fatal("no box node to widen — the fixture stopped exercising this") } out := renderBoth(v) if strings.Contains(out, ">"+long+"<") { t.Error("a title wider than its rect must be truncated, not drawn past the box") } if !strings.Contains(out, "…") { t.Error("truncated text must show an ellipsis") } if !strings.Contains(out, long) { t.Error("the full string must survive as the node's hover title") } } // TestByoBoxIsNotPriced: the platform does not pay for a box it does not // own, so a bring-your-own machine gets no monthly figure and no // "priced by the default spec" chip — a price there would be a claim about // someone else's bill. func TestByoBoxIsNotPriced(t *testing.T) { v := BuildArch(archFixture()) for _, c := range v.Boxes { if c.CloudID != "" { continue } if c.USDMonth != 0 { t.Errorf("BYO box %s priced at %v", c.Label, c.USDMonth) } for _, d := range c.Drift { if strings.Contains(d, "default spec") { t.Errorf("BYO box %s carries a pricing-drift chip: %q", c.Label, d) } } } }