mesh/merge-gate pass: builds networkmanager, systemd-networkd, sent nowhere; no bus step; 4 wait(s) for a person; every machine composes with the change as…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/node-uplink-verbs delivered: every member is delivered
What a machine resolves through had no tool: the resolver file and who wrote it, and every link with its default route and the resolvers its manager knows. Each holder serves the same two node-uplink verbs; the reading is one text carried by both, held to it by a test, and only asking the manager for a link's names is each holder's own.
139 lines
5.0 KiB
Go
139 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The node-uplink seat's verbs (novox/hq ADR 0241), every holder's: the resolver file as it is, the mesh's
|
|
// or another program's — named from its header, a backup beside it or a writer running — and the links
|
|
// with their default route and what the manager knows of their names.
|
|
|
|
const meshFile = "# Managed by the mesh, and written by the module holding this machine's uplink.\n" +
|
|
"nameserver 10.77.0.2\nnameserver 10.77.0.1\noptions timeout:1 attempts:2 edns0\n"
|
|
|
|
const vpnFile = "# Dynamic resolv.conf(5) file for glibc resolver(3) generated by forticlient\n" +
|
|
"# The original file is backed up and will be restored after the VPN disconnects.\n" +
|
|
"nameserver 192.0.2.53\nnameserver 192.0.2.54\nsearch corp.example lab.example\n"
|
|
|
|
func aMachine(t *testing.T, content string) Machine {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "resolv.conf"), []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return Machine{ResolvPath: filepath.Join(dir, "resolv.conf"), Running: func() []string { return nil }}
|
|
}
|
|
|
|
func TestTheMeshsOwnFileIsSaidAsTheMeshs(t *testing.T) {
|
|
r, err := aMachine(t, meshFile).ReadResolvers()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !r.WrittenByTheMesh || r.Writer != "" || len(r.Nameservers) != 2 || r.Nameservers[0] != "10.77.0.2" ||
|
|
strings.Join(r.Options, " ") != "timeout:1 attempts:2 edns0" {
|
|
t.Fatalf("the mesh's file is read as %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestAVPNClientsFileIsReadAndItsWriterNamed(t *testing.T) {
|
|
m := aMachine(t, vpnFile)
|
|
r, err := m.ReadResolvers()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.WrittenByTheMesh || r.Writer != "FortiClient" || strings.Join(r.Search, " ") != "corp.example lab.example" ||
|
|
len(r.Header) != 2 {
|
|
t.Fatalf("the VPN's file is read as %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestABackupBesideTheFileNamesItsWriterAndIsSaidAsTheMeshs(t *testing.T) {
|
|
m := aMachine(t, "nameserver 192.0.2.53\n")
|
|
if err := os.WriteFile(m.ResolvPath+".forticlient.backup", []byte(meshFile), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := m.ReadResolvers()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.Writer != "FortiClient" || len(r.Beside) != 1 || !r.Beside[0].Mesh {
|
|
t.Fatalf("the backup is read as %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestAWriterRunningIsAGuessAndNothingFoundIsSaid(t *testing.T) {
|
|
m := aMachine(t, "nameserver 192.0.2.53\n")
|
|
r, _ := m.ReadResolvers()
|
|
if r.Writer != "" || r.Why != "no program it could be is known" {
|
|
t.Fatalf("nothing to name it by is read as %+v", r)
|
|
}
|
|
m.Running = func() []string { return []string{"bash", "openvpn"} }
|
|
if r, _ := m.ReadResolvers(); r.Writer != "OpenVPN?" {
|
|
t.Fatalf("a running client is read as %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestALinkInPlaceOfTheFileNamesWhatItPointsAt(t *testing.T) {
|
|
m := aMachine(t, meshFile)
|
|
dir := filepath.Join(filepath.Dir(m.ResolvPath), "systemd", "resolve")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "stub-resolv.conf"), []byte("nameserver 127.0.0.53\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.Remove(m.ResolvPath)
|
|
if err := os.Symlink(filepath.Join(dir, "stub-resolv.conf"), m.ResolvPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := m.ReadResolvers()
|
|
if err != nil || r.Writer != "systemd-resolved" || r.WrittenByTheMesh || r.Nameservers[0] != "127.0.0.53" {
|
|
t.Fatalf("a link is read as %+v %v", r, err)
|
|
}
|
|
}
|
|
|
|
func TestTheLinksCarryTheirDefaultRouteAndTheirNames(t *testing.T) {
|
|
m := Machine{
|
|
Run: func(_ context.Context, name string, args ...string) (string, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case joined == "-j address show":
|
|
return `[{"ifname":"lo","operstate":"UNKNOWN","link_type":"loopback","addr_info":[{"local":"127.0.0.1","prefixlen":8}]},
|
|
{"ifname":"wlan0","operstate":"UP","link_type":"ether","addr_info":[{"local":"192.168.1.20","prefixlen":24}]},
|
|
{"ifname":"vpn0","operstate":"UNKNOWN","link_type":"none","addr_info":[{"local":"172.16.9.9","prefixlen":32}]}]`, nil
|
|
case joined == "-j -4 route show default":
|
|
return `[{"dst":"default","dev":"wlan0","metric":600}]`, nil
|
|
case joined == "-j -6 route show default":
|
|
return `[]`, nil
|
|
}
|
|
return "", errors.New("unexpected " + joined)
|
|
},
|
|
LinkDNS: func(context.Context) (map[string]LinkDNS, error) {
|
|
return map[string]LinkDNS{"wlan0": {Servers: []string{"192.168.1.1"}, Manager: "NetworkManager"}}, nil
|
|
},
|
|
}
|
|
answer, err := m.Links(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
links := answer["links"].([]Link)
|
|
if len(links) != 3 || links[0].Name != "wlan0" || !links[0].Default || *links[0].Metric != 600 ||
|
|
links[0].DNS == nil || links[0].DNS.Servers[0] != "192.168.1.1" {
|
|
t.Fatalf("the links are %+v", links)
|
|
}
|
|
for _, l := range links[1:] {
|
|
if l.Default || (l.Name == "vpn0" && l.DNS != nil) {
|
|
t.Fatalf("%+v", l)
|
|
}
|
|
}
|
|
m.LinkDNS = func(context.Context) (map[string]LinkDNS, error) { return nil, errors.New("not running") }
|
|
if answer, _ := m.Links(context.Background()); answer["dns_not_read"] != "not running" {
|
|
t.Fatalf("a manager that does not answer is not said: %+v", answer)
|
|
}
|
|
}
|