package catalogue import ( "strings" "testing" ) // A domain module is a module with requirements and no files of its own. // // Most people want the network working and do not want to choose a VPN. Some want a particular // one. Both are the same mechanism: assigning `networking` takes the only answer to each of its // requirements silently, and the day there are two answers the resolver refuses and names them, // so choosing is assigning the one you want. There is no flavor field and nothing to configure. func networkingShelf(extra ...Manifest) map[string]Manifest { base := []Manifest{ {Module: "networking", Requires: []string{"private-network", "name-resolution"}}, {Module: "mesh-wireguard", Computed: "mesh-wireguard", Provides: Offers("private-network", "mesh-addressing"), Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}}, {Module: "mesh-names", Computed: "mesh-names", Provides: Offers("name-resolution"), Requires: []string{"mesh-addressing"}}, } return shelf(append(base, extra...)...) } func TestOneWordBringsUpTheNetwork(t *testing.T) { // The case that has to stay easy. Nothing is asked, because with one answer to each // requirement there was never a question. got, err := Resolve(networkingShelf(), []string{"networking"}, workstation(), World{}) if err != nil { t.Fatal(err) } have := strings.Join(names(got), " ") for _, want := range []string{"networking", "mesh-wireguard", "mesh-names"} { if !strings.Contains(have, want) { t.Fatalf("assigning networking did not bring in %s: %s", want, have) } } } func TestASecondVPNTurnsItIntoAChoice(t *testing.T) { // And the choice is offered rather than made. A default here would be the flavor field coming // back under another name. _, err := Resolve(networkingShelf( Manifest{Module: "tailscale", Provides: Offers("private-network"), Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}}, ), []string{"networking"}, workstation(), World{}) if err == nil { t.Fatal("two VPNs and one was picked silently") } for _, want := range []string{"mesh-wireguard", "tailscale"} { if !strings.Contains(err.Error(), want) { t.Fatalf("the refusal does not name %s: %v", want, err) } } } func TestChoosingIsAssigning(t *testing.T) { // No second verb. Assigning the one you want answers the requirement, and the bundle takes it. got, err := Resolve(networkingShelf( Manifest{Module: "tailscale", Provides: Offers("private-network", "name-resolution"), Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}}, ), []string{"networking", "tailscale"}, workstation(), World{}) if err != nil { t.Fatal(err) } have := strings.Join(names(got), " ") if !strings.Contains(have, "tailscale") { t.Fatalf("the chosen VPN is not in the set: %s", have) } if strings.Contains(have, "mesh-wireguard") { t.Fatalf("choosing tailscale still installed WireGuard: %s", have) } } func TestChoosingOneVPNCannotDragTheOtherBackIn(t *testing.T) { // This happened. The person chose tailscale; the names module required the mesh's own // addressing; only WireGuard provides that; so both were installed and nobody was told. // // The claim is what catches it. Providing a private network is not the singular part — a // machine could run two VPNs for two purposes — but being *the* one the mesh runs over is. _, err := Resolve(networkingShelf( Manifest{Module: "tailscale", Provides: Offers("private-network"), Claims: []Claim{{Name: "the-private-network", Scope: ScopeNode}}}, ), []string{"networking", "tailscale"}, workstation(), World{}) if err == nil { t.Fatal("a machine was given two private networks without being told") } if !strings.Contains(err.Error(), "the-private-network") { t.Fatalf("the refusal does not say what collided: %v", err) } } func TestNamesNeedTheMeshsOwnAddresses(t *testing.T) { // Names are computed from addresses the mesh handed out. Over a VPN that hands out its own, // the mesh has nothing to write, so the names module requires the addressing rather than a // private network in general — otherwise a machine gets a hosts file full of addresses that // mean nothing on it. _, err := Resolve(shelf( Manifest{Module: "mesh-names", Computed: "mesh-names", Provides: Offers("name-resolution"), Requires: []string{"mesh-addressing"}}, Manifest{Module: "tailscale", Provides: Offers("private-network")}, ), []string{"mesh-names", "tailscale"}, workstation(), World{}) if err == nil { t.Fatal("the mesh's names were installed over a VPN whose addresses it does not hand out") } if !strings.Contains(err.Error(), "mesh-addressing") { t.Fatalf("the refusal does not say what is missing: %v", err) } } func TestARequirementWantedTwiceIsReportedOnce(t *testing.T) { // Two identical lines make a person hunt for the difference between them before realising // there is none. _, err := Resolve(shelf( Manifest{Module: "one", Requires: []string{"shell"}}, Manifest{Module: "two", Requires: []string{"shell"}}, Manifest{Module: "bash", Provides: Offers("shell")}, Manifest{Module: "zsh", Provides: Offers("shell")}, ), []string{"one", "two"}, workstation(), World{}) if err == nil { t.Fatal("two shells and one was picked silently") } if n := strings.Count(err.Error(), `"shell" is wanted by`); n != 1 { t.Fatalf("the same requirement was reported %d times:\n%v", n, err) } }