package catalogue_test import ( "encoding/json" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/overlay" ) // The manifests the control plane actually ships, resolved. // // Written because the earlier tests built their own manifests and passed while the real one was // missing a claim — a whole mechanism could have been absent from what ships and every test would // still have been green. func provided(t *testing.T) map[string]catalogue.Manifest { t.Helper() out := map[string]catalogue.Manifest{} for _, raw := range []map[string]any{ overlay.Manifest(), } { b, err := json.Marshal(raw) if err != nil { t.Fatal(err) } m, err := catalogue.ParseManifest(b) if err != nil { t.Fatalf("a manifest this control plane ships is not valid: %v", err) } out[m.Module] = m } return out } func TestTheShippedPrivateNetworkResolvesOnItsOwn(t *testing.T) { // **Assigned directly** (novox/hq ADR 0226): the `networking` bundle that required it is // retired, so the private network's own module is what a machine is given, and it must need // nothing the control plane does not ship beside it. got, err := catalogue.Resolve(provided(t), []string{overlay.Name}, catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{}) if err != nil { t.Fatalf("assigning %s does not work out of the box: %v", overlay.Name, err) } if len(got.Modules) != 1 || got.Modules[0].Module != overlay.Name { t.Fatalf("assigning %s brought %v", overlay.Name, got.Modules) } // **The network writes no names** (novox/hq ADR 0199): /etc/hosts is the hosts seat holder's // file, and the mesh's resolver answers the machines' names. No fact of the network's module // may name that file. for _, m := range got.Modules { for name, f := range m.Facts { if f.Path == "/etc/hosts" { t.Fatalf("the network's provider still writes /etc/hosts, as its %q fact", name) } } } } func TestTheControlPlaneShipsNoNetworkingBundle(t *testing.T) { // ADR 0226: one module for the one private network. A bundle shipped beside it again would be // a second name every machine carries for the same thing. shipped := provided(t) if len(shipped) != 1 { t.Fatalf("the control plane ships %d modules, want only %s", len(shipped), overlay.Name) } if _, ok := shipped["networking"]; ok { t.Fatal("the retired networking bundle is shipped again") } } func TestARefusalForWantOfThePrivateNetworkNamesItsModule(t *testing.T) { // The hint in a refusal is a string in the resolver rather than an import of this package. It // named `networking` until ADR 0226; a hint naming a module nobody ships sends a person to // assign something that does not exist. shelf := map[string]catalogue.Manifest{ "app": {Module: "app", Version: "1", Requires: []string{"postgres-database"}}, "postgres": {Module: "postgres", Version: "1", Provides: catalogue.FromAnywhere("postgres-database")}, } _, err := catalogue.Resolve(shelf, []string{"app"}, catalogue.Node{Name: "workstation"}, catalogue.World{Offered: map[string][]catalogue.Provider{ "postgres-database": {{Node: "anchor", At: "anchor.internal"}}}}) if err == nil { t.Fatal("an app off the private network was pointed at a database on it") } if !strings.Contains(err.Error(), "assign "+overlay.Name) { t.Fatalf("the refusal does not name the private network's module %s: %v", overlay.Name, err) } } func TestAnotherVPNIsChosenByAssigningItInstead(t *testing.T) { // What the bundle was for, kept without it: a machine given another VPN answers // private-network from that VPN, and WireGuard is not dragged in by anything. shipped := provided(t) shelf := withTailscale(shipped) shelf["needs-network"] = catalogue.Manifest{Module: "needs-network", Version: "1", Requires: []string{overlay.Requirement}} got, err := catalogue.Resolve(shelf, []string{"needs-network", "tailscale"}, catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{}) if err != nil { t.Fatalf("choosing another VPN was refused: %v", err) } for _, m := range got.Modules { if m.Module == overlay.Name { t.Fatalf("the other VPN was chosen and WireGuard came anyway: %v", got.Modules) } } } func TestTwoVPNsAssignedTogetherStillCollide(t *testing.T) { // The claim still guards the case it was always for: both assigned EXPLICITLY, which is a // machine with two private networks and a coin toss about which one a peer reaches it on. shipped := provided(t) _, err := catalogue.Resolve( withTailscale(shipped), []string{overlay.Name, "tailscale"}, catalogue.Node{Name: "workstation", Site: "house"}, catalogue.World{}) if err == nil { t.Fatal("a machine was given two private networks and nobody was told") } if !strings.Contains(err.Error(), overlay.TheNetwork) { t.Fatalf("the refusal does not say what collided: %v", err) } } // The names-need-addressing test went with the names module: names are a fact now, and a machine // the mesh cannot place is simply left out of the file (facts_test.go) — which is the same // protection, enforced where the file is written rather than by a provision refusing. func withTailscale(shelf map[string]catalogue.Manifest) map[string]catalogue.Manifest { out := map[string]catalogue.Manifest{} for k, v := range shelf { out[k] = v } // Deliberately without name-resolution of its own, which is the case that used to install // both VPNs: the names then needed the mesh's addressing, and only WireGuard has it. out["tailscale"] = catalogue.Manifest{ Module: "tailscale", Version: "1", Provides: catalogue.Offers(overlay.Requirement), Claims: []catalogue.Claim{{Name: overlay.TheNetwork, Scope: catalogue.ScopeNode}}, } return out }