package main import ( "encoding/json" "os" "path/filepath" "regexp" "strconv" "strings" "testing" ) // The module's directory, where its manifest and its files/ are. const moduleDir = "../.." // templateText is the text a file resource places on the machine: its template's, in the module's files/ // (novox/hq ADR 0349 §2). A resource that names no template, or a template that is not there, fails the test. func templateText(t *testing.T, r map[string]any) string { t.Helper() name, _ := r["template"].(string) if name == "" { t.Fatalf("resource %v names no template", r["id"]) } raw, err := os.ReadFile(filepath.Join(moduleDir, filepath.FromSlash(name))) if err != nil { t.Fatalf("resource %v names %s: %v", r["id"], name, err) } return string(raw) } // No file resource carries its text inline: each names its template in files/, and the build packs files/ // into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2). func TestEveryFileTakesItsTextFromATemplate(t *testing.T) { raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json")) if err != nil { t.Fatal(err) } var m struct { Resources []map[string]any `json:"resources"` Build struct { Artifacts []map[string]any `json:"artifacts"` } `json:"build"` } if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } for _, r := range m.Resources { if _, inline := r["content"]; inline { t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"]) } if r["type"] == "file" { templateText(t, r) } } packs := 0 for _, a := range m.Build.Artifacts { if a["kind"] == "archive" && a["from"] == "files" { packs++ if a["name"] != "files" { t.Errorf("the templates archive is named %v, not files", a["name"]) } } } if packs != 1 { t.Errorf("the build packs files/ into %d archives, not one", packs) } } // The packaged dnsmasq.service orders itself Before= network-online.target and nss-lookup.target. A drop-in // that orders it After= either of them makes an ordering cycle, and systemd drops the start job: the mesh // resolver stayed down for 90 minutes at a boot (novox/hq issue 1126). Wants=, Requires= and BindsTo= are // refused too: they pull the target in, and with the packaged ordering they are the same mistake. Because the // tunnel is ordered after those targets, the drop-in cannot wait for the address either: a start that finds it // missing fails at once and Restart=on-failure brings it up (see the next test). func TestDropInDoesNotOrderAfterATargetThePackagedUnitOrdersBefore(t *testing.T) { raw, err := os.ReadFile(filepath.Join(moduleDir, "files", "mesh.conf")) if err != nil { t.Fatal(err) } forbidden := []string{"network-online.target", "nss-lookup.target"} for n, line := range strings.Split(string(raw), "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "#") { continue } key, val, ok := strings.Cut(line, "=") key, val = strings.TrimSpace(key), strings.TrimSpace(val) if !ok || (key != "After" && key != "Requires" && key != "Wants" && key != "BindsTo") { continue } for _, target := range forbidden { if strings.Contains(val, target) { t.Errorf("mesh.conf line %d: %s=%s orders dnsmasq after a target its packaged unit orders itself before: a cycle", n+1, key, val) } } } } // The wait for the address in ExecStartPre is at most two seconds: the tunnel that brings the address is ordered // after network-online.target, which the resolver is ordered before, so a longer wait would hold the tunnel back. func TestAddressWaitIsAtMostTwoSeconds(t *testing.T) { raw, err := os.ReadFile(filepath.Join(moduleDir, "files", "mesh.conf")) if err != nil { t.Fatal(err) } found := false for _, line := range strings.Split(string(raw), "\n") { line = strings.TrimSpace(line) if !strings.HasPrefix(line, "ExecStartPre=") { continue } m := regexp.MustCompile(`seq 1 (\d+)`).FindStringSubmatch(line) if m == nil { t.Fatalf("ExecStartPre has no bounded wait (seq 1 N): %s", line) } found = true if n, _ := strconv.Atoi(m[1]); n > 2 { t.Errorf("ExecStartPre waits up to %d s for the address, more than 2 s", n) } } if !found { t.Fatal("mesh.conf has no ExecStartPre") } }