package guesthook import ( "os" "path/filepath" "reflect" "testing" ) const sampleConf = `arch: amd64 cores: 2 hostname: demo-felhom memory: 12288 mp0: local-lvm:vm-9201-disk-1,mp=/var/lib/docker,backup=1,size=256G mp1: /mnt/felhom-usb/felhom-data,mp=/mnt/felhom-usb mp2: /mnt/felhom-flash/felhom-data,mp=/mnt/felhom-flash mp9: /var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1 net0: name=eth0,bridge=vmbr0 rootfs: local-lvm:vm-9201-disk-0,size=32G swap: 4096 unprivileged: 1 ` func TestParseConfMounts(t *testing.T) { got := ParseConfMounts(sampleConf) want := map[string]string{ "mp0": "local-lvm:vm-9201-disk-1", "mp1": "/mnt/felhom-usb/felhom-data", "mp2": "/mnt/felhom-flash/felhom-data", "mp9": "/var/lib/felhom-agent/guests/9201/bootstrap", "rootfs": "local-lvm:vm-9201-disk-0", } if !reflect.DeepEqual(got, want) { t.Fatalf("ParseConfMounts mismatch:\n got=%v\nwant=%v", got, want) } // net0/arch/etc. (non-mount keys) must NOT leak in. if _, bad := got["net0"]; bad { t.Fatalf("net0 was parsed as a mount source") } } // TestMissingBindSources is the load-bearing selector test. The flash drive is absent (its felhom-data // source missing); usb is present; mp0/rootfs are STORAGE volumes (must never be selected); mp9's // bootstrap source is present. Only the flash source may be returned. // // COMPANION GUARD — this test FAILS on the two trivial impls the spec warns about: // - "return nothing" (the pre-fix no-op hook) → flash not selected → guest still bricks → FAIL. // - "return every source" (mkdir everything) → would include the present usb bind AND the // local-lvm storage volumes (creating bogus dirs that shadow real data) → FAIL. func TestMissingBindSources(t *testing.T) { mounts := ParseConfMounts(sampleConf) present := map[string]bool{ "/mnt/felhom-usb/felhom-data": true, // usb attached "/var/lib/felhom-agent/guests/9201/bootstrap": true, // bootstrap always present // "/mnt/felhom-flash/felhom-data" is ABSENT (drive unplugged) } got := MissingBindSources(mounts, func(p string) bool { return present[p] }) want := []string{"/mnt/felhom-flash/felhom-data"} if !reflect.DeepEqual(got, want) { t.Fatalf("MissingBindSources mismatch:\n got=%v\nwant=%v", got, want) } // Explicit companion assertions (pin both failure directions independently of want): for _, p := range got { if p == "/mnt/felhom-usb/felhom-data" { t.Fatalf("selected a PRESENT bind source — over-eager (would shadow live data)") } if p == "local-lvm:vm-9201-disk-1" || p == "local-lvm:vm-9201-disk-0" { t.Fatalf("selected a STORAGE VOLUME source — must only heal host-path binds") } } if len(got) == 0 { t.Fatalf("selected nothing — the absent flash bind would brick the guest (pre-fix no-op)") } } // TestHealCreatesOnlyMissingBind drives the real filesystem path in a temp dir: a present bind source is // left untouched, an absent one is created (so the guest boots), a storage volume is never created. func TestHealCreatesOnlyMissingBind(t *testing.T) { root := t.TempDir() presentSrc := filepath.Join(root, "usb", "felhom-data") absentSrc := filepath.Join(root, "flash", "felhom-data") if err := os.MkdirAll(presentSrc, 0o755); err != nil { t.Fatal(err) } conf := "" + "mp0: local-lvm:vm-9-disk-0,mp=/var/lib/docker,backup=1\n" + "mp1: " + presentSrc + ",mp=/mnt/usb\n" + "mp2: " + absentSrc + ",mp=/mnt/flash\n" + "rootfs: local-lvm:vm-9-disk-1,size=32G\n" confPath := filepath.Join(root, "9.conf") if err := os.WriteFile(confPath, []byte(conf), 0o644); err != nil { t.Fatal(err) } created, err := Heal(confPath) if err != nil { t.Fatalf("Heal: %v", err) } if !reflect.DeepEqual(created, []string{absentSrc}) { t.Fatalf("Heal created %v, want [%s]", created, absentSrc) } if _, err := os.Stat(absentSrc); err != nil { t.Fatalf("absent bind source not created — guest would still brick: %v", err) } // A storage-volume mp must never produce a bogus host directory. if _, err := os.Stat(filepath.Join(root, "local-lvm:vm-9-disk-0")); err == nil { t.Fatalf("a storage volume source was materialised as a directory") } } // TestHealMissingConfNeverErrors — a hook must never block a start, even for an unreadable config. func TestHealMissingConfNeverErrors(t *testing.T) { created, err := Heal(filepath.Join(t.TempDir(), "does-not-exist.conf")) if err != nil || created != nil { t.Fatalf("Heal on missing conf: created=%v err=%v (want nil,nil)", created, err) } }