package modules import ( "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/novox/mesh-control/internal/catalogue" "github.com/novox/mesh-control/internal/overlay" ) // The examples are manifests, so the thing to check is that the catalogue accepts them. // // A manifest that only ever appears in a document is a manifest nobody has run through the parser, // and the parser refuses unknown keys — so a typo here would be discovered by whoever first tried // to use one, which is the opposite of what an example is for. func read(t *testing.T, name string) catalogue.Manifest { t.Helper() raw, err := os.ReadFile(filepath.Join(".", name)) if err != nil { t.Fatal(err) } m, err := catalogue.ParseManifest(raw) if err != nil { t.Fatalf("%s is not a manifest this mesh accepts: %v", name, err) } return m } func TestEveryExampleIsAManifestTheMeshAccepts(t *testing.T) { found, err := filepath.Glob("*.json") if err != nil { t.Fatal(err) } if len(found) == 0 { t.Fatal("no examples, so this test proves nothing") } for _, name := range found { read(t, name) } } // The serving module reads what the mesh writes, and restarts when the mesh rewrites it. // // Without the second it would serve the names it started with for ever — every machine that // joined afterwards unreachable by name, and every check passing. func TestTheResolverReadsTheMeshsNamesAndFollowsThem(t *testing.T) { m := read(t, "dnsmasq.json") var config, service map[string]any for _, r := range m.Resources { switch r["id"] { case "config": config = r case "service": service = r } } if config == nil || service == nil { t.Fatal("the module has no configuration or no service") } if !strings.Contains(config["content"].(string), overlay.ResolverPath) { t.Fatalf("it does not read what the mesh writes at %s", overlay.ResolverPath) } var follows bool for _, id := range service["restart-on"].([]any) { if id.(string) == overlay.Resolver+".nodes" { follows = true } } if !follows { t.Fatalf("it does not restart when the mesh rewrites the names: %v", service["restart-on"]) } } // It binds names the mesh chose, so it needs to know nothing about the machine it is on. // // That is the whole reason these can be static manifests: a resolver must bind somewhere and a // stub must be pointed somewhere, and neither address is knowable in advance — unless the mesh // named it. func TestTheResolverNeedsToKnowNothingAboutItsMachine(t *testing.T) { config := read(t, "dnsmasq.json").Resources[1]["content"].(string) // The directive, not the word: the comment above it names the interface too, so a plain // Contains passes whatever the module actually binds. It did. if !strings.Contains(config, "interface="+overlay.Interface+"\n") { t.Fatalf("it does not bind the private network's interface %q:\n%s", overlay.Interface, config) } if !strings.Contains(config, "listen-address=127.0.0.54\n") { t.Fatalf("it does not answer on the address the asking modules point at:\n%s", config) } // Not the two addresses that belong to something else. for _, taken := range []string{"listen-address=127.0.0.1", "listen-address=127.0.0.53"} { if strings.Contains(config, taken) { t.Fatalf("it takes %q, which belongs to something it did not claim:\n%s", taken, config) } } } // The two ways of deciding what a machine asks claim the same thing, so the mesh refuses the pair. func TestTwoWaysOfOwningTheResolverCannotBothBeAssigned(t *testing.T) { shelf := map[string]catalogue.Manifest{} for _, name := range []string{"resolved-split-dns.json", "resolv-conf.json", "dnsmasq.json"} { m := read(t, name) shelf[m.Module] = m } // Something has to answer `wildcard-resolution`, or they are refused for that instead and the // test would pass without ever reaching the claim. shelf["dnsmasq"] = read(t, "dnsmasq.json") _, err := catalogue.Resolve(shelf, []string{"dnsmasq", "resolved-split-dns", "resolv-conf"}, catalogue.Node{Name: "anchor", Capabilities: map[string]bool{}}, catalogue.World{Unchecked: true}) if err == nil { t.Fatal("both ways of owning the resolver were assigned to one machine") } said := err.Error() if !strings.Contains(said, "the-resolver-configuration") { t.Fatalf("the refusal does not name what they both want: %v", said) } } // And the two roles are not the same claim: a machine runs one resolver AND one thing deciding // what it asks, so serving and asking must be assignable together. func TestServingAndAskingAreAssignableTogether(t *testing.T) { shelf := map[string]catalogue.Manifest{} for _, name := range []string{"dnsmasq.json", "resolved-split-dns.json"} { m := read(t, name) shelf[m.Module] = m } if _, err := catalogue.Resolve(shelf, []string{"dnsmasq", "resolved-split-dns"}, catalogue.Node{Name: "anchor", Capabilities: map[string]bool{}}, catalogue.World{Unchecked: true}); err != nil { t.Fatalf("a resolver and the thing pointing at it cannot both be assigned: %v", err) } } // The examples are JSON a person edits, so a stray comma is worth catching here rather than on a // machine. func TestTheExamplesAreWellFormed(t *testing.T) { found, _ := filepath.Glob("*.json") for _, name := range found { raw, err := os.ReadFile(name) if err != nil { t.Fatal(err) } var any map[string]any if err := json.Unmarshal(raw, &any); err != nil { t.Fatalf("%s is not JSON: %v", name, err) } } }