package app import ( "context" "errors" "testing" "github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/ports" "github.com/dtoro/oikos/internal/core/ports/portstest" ) func newProvisioningTestDeps() (*portstest.FakeProvisioner, *portstest.FakeResolver, *portstest.EntityRepo, *portstest.RelRepo, *ProvisioningService) { prov := &portstest.FakeProvisioner{LXCRes: ports.ProvisionResult{VMID: 142, Template: "debian-13-standard_13.0-1_amd64.tar.zst", Output: "create ok"}} resolver := &portstest.FakeResolver{Addr: "10.0.0.5", User: "root"} entities := portstest.NewEntityRepo() rels := portstest.NewRelRepo() entities.Create(context.Background(), ports.EntityCreateInput{ Entity: domain.Entity{ID: "host-1", Slug: "host:hubris", Type: "proxmox-host", Name: "hubris", State: "active"}, }) svc := NewProvisioningService(prov, resolver, entities, rels) return prov, resolver, entities, rels, svc } func TestProvisioningCreateLXCRequiresHostname(t *testing.T) { _, _, _, _, svc := newProvisioningTestDeps() _, err := svc.CreateLXC(context.Background(), CreateLXCCmd{HostSlug: "host:hubris"}) if err == nil || err.Error() != "pct_create: hostname is required" { t.Fatalf("got %v, want hostname-required error", err) } } func TestProvisioningCreateLXCHappyPath(t *testing.T) { prov, resolver, entities, rels, svc := newProvisioningTestDeps() out, err := svc.CreateLXC(context.Background(), CreateLXCCmd{ HostSlug: "host:hubris", Hostname: "grafana", IP: "192.168.8.55", }) if err != nil { t.Fatalf("CreateLXC: %v", err) } if out.Slug != "lxc:grafana" || out.VMID != 142 { t.Errorf("outcome = %+v, want slug lxc:grafana vmid 142", out) } // Provisioner saw the resolved endpoint and the defaults. if len(prov.LXCs) != 1 { t.Fatalf("provisioner calls = %d, want 1", len(prov.LXCs)) } in := prov.LXCs[0] if in.HostAddr != "10.0.0.5" || in.HostUser != "root" { t.Errorf("resolved endpoint = %s@%s, want root@10.0.0.5", in.HostUser, in.HostAddr) } if in.Cores != 1 || in.MemoryMB != 512 || in.DiskGB != 8 || in.Storage != "local-lvm" { t.Errorf("defaults not applied: %+v", in) } if in.Bridge != "vmbr0" || in.GW != "192.168.8.2" || in.Searchdomain != "hubris.network" { t.Errorf("network defaults not applied: %+v", in) } // Entity registered: provisioning state, enrolled, vmid/host attrs. got, ok := entities.FindBySlug("lxc:grafana") if !ok { t.Fatal("lxc:grafana not registered") } if got.Type != "lxc" || got.State != "provisioning" { t.Errorf("entity = %s/%s, want lxc/provisioning", got.Type, got.State) } if got.Attributes["pve_id"] != "142" || got.Attributes["host"] != "hubris" || got.Attributes["ip"] != "192.168.8.55" { t.Errorf("attrs = %v", got.Attributes) } if _, enrolled := entities.Enrolled[got.ID]; !enrolled { t.Error("entity not enrolled") } // Hosts edge from the Proxmox host to the guest. edges, err := rels.ListFor(context.Background(), domain.UUID("host-1"), "outbound") if err != nil || len(edges) != 1 { t.Fatalf("host edges = %v err=%v, want 1", edges, err) } if edges[0].Type != "hosts" || edges[0].TargetID != got.ID { t.Errorf("edge = %+v, want hosts → %s", edges[0], got.ID) } if edges[0].Attributes["provisioned_by"] != "nomos" { t.Errorf("edge attrs = %v, want provisioned_by=nomos", edges[0].Attributes) } _ = resolver // endpoint asserted via provisioner input } func TestProvisioningCreateLXCProvisionerFailureRegistersNothing(t *testing.T) { prov, _, entities, rels, svc := newProvisioningTestDeps() prov.LXCErr = errors.New("no usable LXC template") if _, err := svc.CreateLXC(context.Background(), CreateLXCCmd{HostSlug: "host:hubris", Hostname: "x"}); err == nil { t.Fatal("expected provisioner error to propagate") } if _, ok := entities.FindBySlug("lxc:x"); ok { t.Error("failed create must not register an entity") } if edges, _ := rels.ListFor(context.Background(), domain.UUID("host-1"), "outbound"); len(edges) != 0 { t.Errorf("failed create must not create edges, got %v", edges) } } func TestProvisioningCreateLXCRegistrationBestEffort(t *testing.T) { _, _, entities, rels, svc := newProvisioningTestDeps() entities.ErrStub = errors.New("db down") // The container exists on the host; a graph write failure must not // fail the provisioning outcome. out, err := svc.CreateLXC(context.Background(), CreateLXCCmd{HostSlug: "host:hubris", Hostname: "y"}) if err != nil { t.Fatalf("CreateLXC should survive registration failure: %v", err) } if out.VMID != 142 { t.Errorf("outcome = %+v, want provisioner result", out) } if edges, _ := rels.ListFor(context.Background(), domain.UUID("host-1"), "outbound"); len(edges) != 0 { t.Errorf("no edges expected when entity write failed, got %v", edges) } }