package impact import ( "reflect" "testing" ) func testIndex(t *testing.T) *Index { t.Helper() idx, err := BuildIndex("testdata/repo") if err != nil { t.Fatalf("BuildIndex: %v", err) } return idx } func resolvedHosts(t *testing.T, class string) []string { t.Helper() res := newClassResolver(testIndex(t), fixtureNodes()).resolve(class) hosts := make([]string, 0, len(res.hosts)) for certname := range res.hosts { hosts = append(hosts, certname) } return uniqueSorted(hosts) } func TestResolveThroughIncludeChain(t *testing.T) { // profiles::leaf <- profiles::mid <- profiles::base <- two role classes. if got, want := resolvedHosts(t, "profiles::leaf"), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) { t.Errorf("profiles::leaf = %v, want %v", got, want) } } func TestResolveContainAndClassResource(t *testing.T) { for _, class := range []string{"profiles::contained", "profiles::viaclass"} { if got, want := resolvedHosts(t, class), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) { t.Errorf("%s = %v, want %v", class, got, want) } } } func TestResolveDefinedTypeUsage(t *testing.T) { if got, want := resolvedHosts(t, "profiles::defined::thing"), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) { t.Errorf("profiles::defined::thing = %v, want %v", got, want) } } func TestResolveTerminatesOnCycle(t *testing.T) { // profiles::cycle::a and ::b include each other; ::a is pulled in by // profiles::base, so the walk must reach the roles and stop. if got, want := resolvedHosts(t, "profiles::cycle::b"), []string{"k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) { t.Errorf("profiles::cycle::b = %v, want %v", got, want) } } func TestResolveViaHieraIncludeOnly(t *testing.T) { // The jellyfin module is applied by hiera_include in a role yaml and by // nothing in any manifest — a .pp-only search would call it orphaned. if got, want := resolvedHosts(t, "jellyfin"), []string{"web1"}; !reflect.DeepEqual(got, want) { t.Errorf("jellyfin = %v, want %v", got, want) } // One hop further in: jellyfin::install is only reachable through jellyfin. if got, want := resolvedHosts(t, "jellyfin::install"), []string{"web1"}; !reflect.DeepEqual(got, want) { t.Errorf("jellyfin::install = %v, want %v", got, want) } } func TestResolveHieraIncludeFromVirtualHierarchy(t *testing.T) { if got, want := resolvedHosts(t, "profiles::lldpd"), []string{"k8s1", "k8s2", "prox1"}; !reflect.DeepEqual(got, want) { t.Errorf("profiles::lldpd = %v, want %v", got, want) } } func TestResolveHonoursHieraExclude(t *testing.T) { // common.yaml pulls basemod in for everyone; roles/infra/proxmox.yaml // excludes it again, so the proxmox node drops out. if got, want := resolvedHosts(t, "basemod"), []string{"bare1", "k8s1", "k8s2", "web1"}; !reflect.DeepEqual(got, want) { t.Errorf("basemod = %v, want %v", got, want) } } func TestResolveOrphanClass(t *testing.T) { res := newClassResolver(testIndex(t), fixtureNodes()).resolve("orphanmod") if !res.orphan { t.Errorf("orphanmod should be orphaned, hosts = %v", res.hosts) } if len(res.hosts) != 0 { t.Errorf("orphanmod reached %v", res.hosts) } } func TestIndexIgnoresCommentedReferences(t *testing.T) { if got := testIndex(t).IncludedBy("profiles::commented"); len(got) != 0 { t.Errorf("a commented-out class name was indexed as a reference: %v", got) } } func TestIndexAssetOwners(t *testing.T) { idx := testIndex(t) for _, asset := range []string{"profiles/leaf/x.erb", "profiles/leaf/y.txt"} { if got, want := idx.AssetOwners(asset), []string{"profiles::leaf"}; !reflect.DeepEqual(got, want) { t.Errorf("AssetOwners(%q) = %v, want %v", asset, got, want) } } } func TestIndexModuleClasses(t *testing.T) { got := testIndex(t).ModuleClasses("jellyfin") want := []string{"jellyfin", "jellyfin::init", "jellyfin::install"} if !reflect.DeepEqual(got, want) { t.Errorf("ModuleClasses(jellyfin) = %v, want %v", got, want) } } func TestParseHieraListsRegexFallback(t *testing.T) { // eyaml payloads a strict parser rejects still have to yield their lists. broken := []byte("---\nhiera_include:\n - alpha\n - beta\nhiera_exclude:\n - gamma\nbad: [unclosed\n") include, exclude := parseHieraLists(broken) if !reflect.DeepEqual(include, []string{"alpha", "beta"}) { t.Errorf("include = %v", include) } if !reflect.DeepEqual(exclude, []string{"gamma"}) { t.Errorf("exclude = %v", exclude) } } func TestResolveInheritsScopeVarCreateResourcesAndInlineDefine(t *testing.T) { // Four reference forms a plain include grep misses: class inheritance, // a $class::var read, create_resources() naming a type in a string, and a // defined type declared part-way along a line. tests := map[string]string{ "inherits": "profiles::inherited", "scope variable": "profiles::scopevar", "create_resources": "profiles::created", "inline define": "profiles::inline::thing", } want := []string{"k8s1", "k8s2", "web1"} for name, class := range tests { t.Run(name, func(t *testing.T) { if got := resolvedHosts(t, class); !reflect.DeepEqual(got, want) { t.Errorf("%s = %v, want %v", class, got, want) } }) } } func TestResolveEveryNameInAnIncludeList(t *testing.T) { // profiles::base pulls both classes in with one comma-separated include; // the second name must not be dropped. want := []string{"k8s1", "k8s2", "web1"} for _, class := range []string{"profiles::listed::a", "profiles::listed::b"} { if got := resolvedHosts(t, class); !reflect.DeepEqual(got, want) { t.Errorf("%s = %v, want %v", class, got, want) } } }