package ontology import ( "errors" "testing" "github.com/dtoro/oikos/internal/domain" ) func fixtureTree() *TypeTree { return &TypeTree{ Types: map[string]TypeInfo{ "entity": {IsAbstract: true}, "compute-entity": {Parent: "entity", IsAbstract: true}, "machine": {Parent: "compute-entity", IsAbstract: true}, "proxmox-host": {Parent: "machine", LifecycleID: "infrastructure"}, "lxc": {Parent: "compute-entity", LifecycleID: "infrastructure"}, "service": {Parent: "entity", LifecycleID: "infrastructure"}, "document": {Parent: "entity"}, // no lifecycle }, RelTypes: map[string]RelTypeInfo{ "hosts": {SourceType: "machine", TargetType: "compute-entity", Cardinality: "one-to-many"}, "provides": {SourceType: "compute-entity", TargetType: "service", Cardinality: "one-to-many"}, "depends-on": {SourceType: "service", TargetType: "service", Cardinality: "many-to-many"}, "documents": {SourceType: "document", TargetType: "entity", Cardinality: "many-to-one"}, }, Lifecycles: map[string]LifecycleInfo{ "infrastructure": { States: map[string]bool{"planned": true, "active": true, "destroyed": true}, DefaultState: "active", }, }, } } func TestIsAWalksHierarchy(t *testing.T) { tree := fixtureTree() cases := []struct { typ, target string want bool }{ {"proxmox-host", "machine", true}, {"proxmox-host", "compute-entity", true}, {"proxmox-host", "entity", true}, {"proxmox-host", "proxmox-host", true}, {"lxc", "machine", false}, {"service", "compute-entity", false}, {"nonexistent", "entity", false}, } for _, c := range cases { if got := tree.IsA(c.typ, c.target); got != c.want { t.Errorf("IsA(%q, %q) = %v, want %v", c.typ, c.target, got, c.want) } } } func TestValidateEntityRejectsAbstract(t *testing.T) { tree := fixtureTree() for _, abstract := range []string{"entity", "compute-entity", "machine"} { if err := tree.ValidateEntity(abstract, ""); !errors.Is(err, domain.ErrAbstractType) { t.Errorf("ValidateEntity(%q) = %v, want ErrAbstractType", abstract, err) } } if err := tree.ValidateEntity("lxc", "active"); err != nil { t.Errorf("ValidateEntity(lxc, active) = %v, want nil", err) } } func TestValidateEntityStates(t *testing.T) { tree := fixtureTree() if err := tree.ValidateEntity("lxc", "flying"); !errors.Is(err, domain.ErrInvalidTransition) { t.Errorf("bad state = %v, want ErrInvalidTransition", err) } // state on a type without a lifecycle is rejected if err := tree.ValidateEntity("document", "active"); !errors.Is(err, domain.ErrInvalidTransition) { t.Errorf("state without lifecycle = %v, want ErrInvalidTransition", err) } // unknown type if err := tree.ValidateEntity("ghost", ""); !errors.Is(err, domain.ErrNotFound) { t.Errorf("unknown type = %v, want ErrNotFound", err) } } func TestValidateEdgeHonorsInheritance(t *testing.T) { tree := fixtureTree() // proxmox-host is-a machine; lxc is-a compute-entity → valid if err := tree.ValidateEdge("hosts", "proxmox-host", "lxc"); err != nil { t.Errorf("hosts(proxmox-host→lxc) = %v, want nil", err) } // abstract endpoint declared, concrete descendant offered → valid if err := tree.ValidateEdge("provides", "lxc", "service"); err != nil { t.Errorf("provides(lxc→service) = %v, want nil", err) } // documents targets the root abstract 'entity' → anything is valid if err := tree.ValidateEdge("documents", "document", "proxmox-host"); err != nil { t.Errorf("documents(document→proxmox-host) = %v, want nil", err) } // service is not a machine → invalid source if err := tree.ValidateEdge("hosts", "service", "lxc"); !errors.Is(err, domain.ErrInvalidEdge) { t.Errorf("hosts(service→lxc) = %v, want ErrInvalidEdge", err) } // lxc is not a service → invalid target if err := tree.ValidateEdge("depends-on", "service", "lxc"); !errors.Is(err, domain.ErrInvalidEdge) { t.Errorf("depends-on(service→lxc) = %v, want ErrInvalidEdge", err) } // unknown relationship type if err := tree.ValidateEdge("teleports", "lxc", "service"); !errors.Is(err, domain.ErrNotFound) { t.Errorf("unknown rel type = %v, want ErrNotFound", err) } } func TestDefaultState(t *testing.T) { tree := fixtureTree() if got := tree.DefaultState("lxc"); got != "active" { t.Errorf("DefaultState(lxc) = %q, want active", got) } if got := tree.DefaultState("document"); got != "" { t.Errorf("DefaultState(document) = %q, want empty", got) } }