package daemon import ( "context" "path/filepath" "strings" "testing" "banger/internal/model" "banger/internal/paths" ) // TestReserveVMAllowsNameThatPrefixesExistingVM is a regression for a // correctness bug in the name-uniqueness check: reserveVM used to // route through FindVM, which falls back to prefix-matching on both // ids and names. That meant a perfectly valid new name like "beta" // could be rejected simply because an existing VM's id or name // started with "beta". Exact-name lookup via store.GetVMByName fixes // it. The test seeds a VM whose id and name are long strings, then // tries to reserve a new VM with a name that's a prefix of each — // both must succeed. func TestReserveVMAllowsNameThatPrefixesExistingVM(t *testing.T) { ctx := context.Background() tmp := t.TempDir() d := &Daemon{ store: openDaemonStore(t), layout: paths.Layout{VMsDir: filepath.Join(tmp, "vms"), RuntimeDir: filepath.Join(tmp, "runtime")}, config: model.DaemonConfig{BridgeIP: model.DefaultBridgeIP}, } wireServices(d) existing := testVM("longname-sandbox-foobar", "image-x", "172.16.0.50") upsertDaemonVM(t, ctx, d.store, existing) image := testImage("image-x") image.ID = "image-x" image.Name = "image-x" if err := d.store.UpsertImage(ctx, image); err != nil { t.Fatalf("UpsertImage: %v", err) } // New VM name is a prefix of the existing id (which is // "longname-sandbox-foobar-id" per testVM). Old FindVM-based check // would reject this. if vm, err := d.vm.reserveVM(ctx, "longname", image, model.VMSpec{VCPUCount: 1, MemoryMiB: 128}); err != nil { t.Fatalf("reserveVM(prefix of id): %v", err) } else if vm.Name != "longname" { t.Fatalf("reserveVM returned name=%q, want longname", vm.Name) } // Prefix of the existing name ("longname-sandbox") must also work. if vm, err := d.vm.reserveVM(ctx, "longname-sandbox", image, model.VMSpec{VCPUCount: 1, MemoryMiB: 128}); err != nil { t.Fatalf("reserveVM(prefix of name): %v", err) } else if vm.Name != "longname-sandbox" { t.Fatalf("reserveVM returned name=%q, want longname-sandbox", vm.Name) } } // TestReserveVMRejectsExactDuplicateName confirms the uniqueness // check still catches actual collisions after the FindVM → GetVMByName // switch. func TestReserveVMRejectsExactDuplicateName(t *testing.T) { ctx := context.Background() tmp := t.TempDir() d := &Daemon{ store: openDaemonStore(t), layout: paths.Layout{VMsDir: filepath.Join(tmp, "vms"), RuntimeDir: filepath.Join(tmp, "runtime")}, config: model.DaemonConfig{BridgeIP: model.DefaultBridgeIP}, } wireServices(d) existing := testVM("sandbox", "image-x", "172.16.0.51") upsertDaemonVM(t, ctx, d.store, existing) image := testImage("image-x") image.ID = "image-x" image.Name = "image-x" if err := d.store.UpsertImage(ctx, image); err != nil { t.Fatalf("UpsertImage: %v", err) } _, err := d.vm.reserveVM(ctx, "sandbox", image, model.VMSpec{VCPUCount: 1, MemoryMiB: 128}) if err == nil { t.Fatal("reserveVM with duplicate name should have failed") } if !strings.Contains(err.Error(), "already exists") { t.Fatalf("err = %v, want 'already exists'", err) } } // TestReserveVMRejectsInvalidName pins defense-in-depth: the CLI // already validates, but any other RPC caller (banger SDK, direct // JSON over the socket) lands here without going through the CLI. // The name ends up in /etc/hostname, kernel boot args, DNS records, // and file paths — the daemon must refuse anything that isn't a // valid DNS label. func TestReserveVMRejectsInvalidName(t *testing.T) { ctx := context.Background() tmp := t.TempDir() d := &Daemon{ store: openDaemonStore(t), layout: paths.Layout{VMsDir: filepath.Join(tmp, "vms"), RuntimeDir: filepath.Join(tmp, "runtime")}, config: model.DaemonConfig{BridgeIP: model.DefaultBridgeIP}, } wireServices(d) image := testImage("image-x") image.ID = "image-x" image.Name = "image-x" if err := d.store.UpsertImage(ctx, image); err != nil { t.Fatalf("UpsertImage: %v", err) } for _, bad := range []string{ "MyBox", // uppercase "my box", // space "my.box", // dot "box\n", // newline "-box", // leading hyphen "box/../evil", // path separator + traversal } { if _, err := d.vm.reserveVM(ctx, bad, image, model.VMSpec{VCPUCount: 1, MemoryMiB: 128}); err == nil { t.Fatalf("reserveVM(%q) = nil error, want rejection", bad) } } }