Files
mesh-catalog/modules/dnsmasq/cmd/dnsmasq-tools/templates_test.go
T
jschoubben c88be88ccf
mesh/delivery merged: source.main moved to novox/mesh-catalog@94805575379f
mesh/check checking c88be88ccf2f on 94805575379f
dnsmasq's public upstream resolvers are config values, defaulting to the two it always used
The two server lines were literals in the template, so changing an upstream needed a build. They are now the config keys upstream-resolver-first (1.1.1.1) and upstream-resolver-second (8.8.8.8), placed with ${config:...}; the service already restarts on its config file. no-resolv and every other line stay.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-08 23:48:59 +01:00

165 lines
5.6 KiB
Go

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")
}
}
// The two public upstreams are the module's settings, defaulting to the two the resolver always used, placed
// as server lines beside no-resolv: a person sets them per node or mesh-wide without a build.
func TestUpstreamsAreConfigValues(t *testing.T) {
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if err != nil {
t.Fatal(err)
}
var m struct {
Config map[string]struct {
Default string `json:"default"`
Pattern string `json:"pattern"`
} `json:"config"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
conf, err := os.ReadFile(filepath.Join(moduleDir, "files", "dnsmasq.conf"))
if err != nil {
t.Fatal(err)
}
for key, def := range map[string]string{"upstream-resolver-first": "1.1.1.1", "upstream-resolver-second": "8.8.8.8"} {
c, ok := m.Config[key]
if !ok || c.Default != def {
t.Errorf("config %s = %+v, want default %s", key, c, def)
}
if !regexp.MustCompile(c.Pattern).MatchString(def) {
t.Errorf("pattern %q refuses the default %s", c.Pattern, def)
}
if !strings.Contains(string(conf), "\nserver=${config:"+key+"}\n") {
t.Errorf("dnsmasq.conf does not place %s as a server line", key)
}
}
if strings.Contains(string(conf), "server=1.1.1.1") || strings.Contains(string(conf), "server=8.8.8.8") {
t.Error("dnsmasq.conf still hard-codes an upstream")
}
if !strings.Contains(string(conf), "\nno-resolv\n") {
t.Error("dnsmasq.conf lost no-resolv")
}
}