Files
felhom-agent/internal/storage/hostops_disk_test.go
T
admin 769c4c3cf2 R-25 (agent half): the format answer carries the NEW filesystem's UUID, bound to the durable id
After mkfs the agent re-resolves the bound durable id, requires it to name the
device it just formatted, reads the superblock back (blkid -p, requested fstype)
and returns fs_uuid in POST /disks/format and GET /disks/format/status. Anything
unverified returns "" — never a path-resolved guess. The controller half
(mount fs_uuid instead of re-resolving the UUID from the /dev path) is owed
in felhom-controller.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-05 21:16:11 +02:00

225 lines
7.3 KiB
Go

package storage
import (
"context"
"errors"
"io"
"strings"
"testing"
)
// scriptedRunner returns canned stdout/stderr/err per command name (last arg = device).
type scriptedRunner struct {
calls [][]string
outputs map[string][]byte // keyed by binary basename
errs map[string]error
}
func (r *scriptedRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
return r.Run(ctx, name, args...)
}
func (r *scriptedRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
r.calls = append(r.calls, append([]string{name}, args...))
base := name
if i := strings.LastIndexByte(name, '/'); i >= 0 {
base = name[i+1:]
}
return r.outputs[base], nil, r.errs[base]
}
func (r *scriptedRunner) ran(substr string) bool {
for _, c := range r.calls {
if strings.Contains(strings.Join(c, " "), substr) {
return true
}
}
return false
}
func newSudo(r *scriptedRunner) *SudoHostOps {
return NewSudoHostOps(SudoHostOpsConfig{Runner: r})
}
// ---- validators -------------------------------------------------------------------------
func TestValidateBlockDevice(t *testing.T) {
ok := []string{"/dev/sdb", "/dev/sdb1", "/dev/nvme0n1", "/dev/nvme0n1p2", "/dev/vdb3"}
for _, d := range ok {
if err := ValidateBlockDevice(d); err != nil {
t.Errorf("expected %q valid: %v", d, err)
}
}
bad := []string{"/dev/disk/by-uuid/x", "/dev/../etc/passwd", "/dev/sdb; rm -rf /", "/etc/shadow", "/dev/mapper/x", "sdb", ""}
for _, d := range bad {
if err := ValidateBlockDevice(d); err == nil {
t.Errorf("expected %q rejected", d)
}
}
}
func TestValidateFSType(t *testing.T) {
for _, f := range []string{"ext4", "xfs"} {
if err := ValidateFSType(f); err != nil {
t.Errorf("expected %q valid", f)
}
}
for _, f := range []string{"ntfs", "vfat", "ext4 ", "", "ext4;ls"} {
if err := ValidateFSType(f); err == nil {
t.Errorf("expected %q rejected", f)
}
}
}
// ---- InspectDevice (data-bearing detection) ---------------------------------------------
func TestInspect_Blank(t *testing.T) {
// blkid finds nothing (empty output); lsblk reads cleanly and shows a bare disk.
r := &scriptedRunner{
outputs: map[string][]byte{
"blkid": nil,
"lsblk": []byte(`{"blockdevices":[{"name":"sdb","fstype":null,"pttype":null,"mountpoint":null}]}`),
},
errs: map[string]error{"blkid": errors.New("exit status 2")}, // blkid exits non-zero on blank
}
p, err := newSudo(r).InspectDevice(context.Background(), "/dev/sdb")
if err != nil {
t.Fatal(err)
}
if !p.Probed {
t.Fatal("expected a clean probe (lsblk read cleanly)")
}
if p.DataBearing() {
t.Fatalf("blank device classified data-bearing: %+v", p)
}
}
func TestInspect_HasFilesystem(t *testing.T) {
r := &scriptedRunner{
outputs: map[string][]byte{
"blkid": []byte("DEVNAME=/dev/sdb\nTYPE=ext4\nUSAGE=filesystem\n"),
"lsblk": []byte(`{"blockdevices":[{"name":"sdb","fstype":"ext4","pttype":null,"mountpoint":null}]}`),
},
}
p, _ := newSudo(r).InspectDevice(context.Background(), "/dev/sdb")
if !p.DataBearing() || !p.HasFilesystem || p.FSType != "ext4" {
t.Fatalf("filesystem not detected: %+v", p)
}
}
func TestInspect_HasPartitionTable(t *testing.T) {
r := &scriptedRunner{
outputs: map[string][]byte{
"blkid": []byte("DEVNAME=/dev/sdb\nPTTYPE=gpt\n"),
"lsblk": []byte(`{"blockdevices":[{"name":"sdb","pttype":"gpt","children":[{"name":"sdb1","fstype":"ext4"}]}]}`),
},
}
p, _ := newSudo(r).InspectDevice(context.Background(), "/dev/sdb")
if !p.DataBearing() || !p.HasPartitionTable || !p.HasPartitions {
t.Fatalf("partition table/children not detected: %+v", p)
}
}
func TestInspect_Mounted(t *testing.T) {
r := &scriptedRunner{
outputs: map[string][]byte{
"blkid": []byte("TYPE=xfs\n"),
"lsblk": []byte(`{"blockdevices":[{"name":"sdb","fstype":"xfs","mountpoint":"/mnt/data"}]}`),
},
}
p, _ := newSudo(r).InspectDevice(context.Background(), "/dev/sdb")
if !p.Mounted || !p.DataBearing() {
t.Fatalf("mounted not detected: %+v", p)
}
}
// A probe that fails to read cleanly must be conservative (data-bearing).
func TestInspect_FailedProbe_FailSafe(t *testing.T) {
r := &scriptedRunner{
outputs: map[string][]byte{"blkid": nil, "lsblk": nil},
errs: map[string]error{"blkid": errors.New("blkid broke"), "lsblk": errors.New("lsblk broke")},
}
p, _ := newSudo(r).InspectDevice(context.Background(), "/dev/sdb")
if p.Probed {
t.Fatal("a broken probe must not be 'Probed'")
}
if !p.DataBearing() {
t.Fatal("a broken probe must be treated as data-bearing (fail-safe)")
}
}
func TestInspect_RejectsBadDevice(t *testing.T) {
if _, err := newSudo(&scriptedRunner{}).InspectDevice(context.Background(), "/dev/../etc"); err == nil {
t.Fatal("expected a bad device to be rejected before any exec")
}
}
// ---- Format (mkfs) ----------------------------------------------------------------------
// Format now (Impl-1) runs the unclaimed guard then execs the guarded wrapper (<device> <fstype>),
// not raw mkfs. These use the stubbed guard (unclaimed /dev/sdb; system disk = /dev/sda).
func TestFormat_Ext4(t *testing.T) {
defer stubRO(false)()
sr := &scriptRunner{out: map[string][]byte{
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sdb"}]}`),
}}
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
if err := ops.Format(context.Background(), "/dev/sdb", "ext4"); err != nil {
t.Fatal(err)
}
if !srRan(sr, "/usr/local/sbin/felhom-mkfs-guarded /dev/sdb ext4") {
t.Fatalf("guarded wrapper not invoked correctly: %v", sr.calls)
}
}
func TestFormat_Xfs(t *testing.T) {
defer stubRO(false)()
sr := &scriptRunner{out: map[string][]byte{
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"nvme0n1","children":[{"name":"nvme0n1p1"}]}]}`),
}}
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
if err := ops.Format(context.Background(), "/dev/nvme0n1p1", "xfs"); err != nil {
t.Fatal(err)
}
if !srRan(sr, "/usr/local/sbin/felhom-mkfs-guarded /dev/nvme0n1p1 xfs") {
t.Fatalf("guarded wrapper not invoked correctly: %v", sr.calls)
}
}
// srRan reports whether the scriptRunner recorded a call whose joined form contains substr.
func srRan(sr *scriptRunner, substr string) bool {
for _, c := range sr.calls {
if strings.Contains(strings.Join(c, " "), substr) {
return true
}
}
return false
}
func TestFormat_RejectsBadArgs(t *testing.T) {
r := &scriptedRunner{}
if err := newSudo(r).Format(context.Background(), "/dev/disk/by-uuid/x", "ext4"); err == nil {
t.Fatal("expected bad device rejected")
}
if err := newSudo(r).Format(context.Background(), "/dev/sdb", "ntfs"); err == nil {
t.Fatal("expected bad fstype rejected")
}
if len(r.calls) != 0 {
t.Fatalf("mkfs ran despite invalid input: %v", r.calls)
}
}
// R-25: the probe carries the superblock's filesystem UUID so the format path can report the new one.
func TestInspect_ReadsFilesystemUUID(t *testing.T) {
r := &scriptedRunner{
outputs: map[string][]byte{
"blkid": []byte("DEVNAME=/dev/sdb\nUUID=0fc63daf-8483-4772-8e79-3d69d8477de4\nTYPE=ext4\nUSAGE=filesystem\n"),
"lsblk": []byte(`{"blockdevices":[{"name":"sdb","fstype":"ext4","pttype":null,"mountpoint":null}]}`),
},
}
p, _ := newSudo(r).InspectDevice(context.Background(), "/dev/sdb")
if p.FSUUID != "0fc63daf-8483-4772-8e79-3d69d8477de4" {
t.Fatalf("FSUUID = %q, want the blkid UUID", p.FSUUID)
}
}