From 1b70c1c17b96b15870ff7a1260449cbf1b093ae4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 08:30:57 +0000 Subject: [PATCH] Add unit tests for extractPath, extractHierarchy, and printHierarchy Co-authored-by: bosvos <2437699+bosvos@users.noreply.github.com> PR #83 Changes --- ldap.go | 15 ++-- ldap_test.go | 233 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 5 deletions(-) diff --git a/ldap.go b/ldap.go index d4b30d9..803b81b 100644 --- a/ldap.go +++ b/ldap.go @@ -197,17 +197,22 @@ type pathElement struct { nodeType string } +// formatHierarchyPath returns the formatted path string for a node. +// If nodeType is "CN" and printNodesIfCN is true, uses colon separator. +func formatHierarchyPath(pathPrefix, nodeName, nodeType string, printNodesIfCN bool) string { + if nodeType == "CN" && printNodesIfCN { + return printTrunc(pathPrefix+" : "+nodeName, 120, "...") + } + return printTrunc(pathPrefix+"/"+nodeName, 120, "...") +} + func printHierarchy(hierarchy *hierarchyNode, pathPrefix string, printNodesIfCN bool, log *log.Logger) { if hierarchy == nil { return } currentNode := hierarchy // print current node - if currentNode.nodeType == "CN" && printNodesIfCN { - log.Infof("Node: %s\n", printTrunc(pathPrefix+" : "+currentNode.name, 120, "...")) - } else { - log.Infof("Node: %s\n", printTrunc(pathPrefix+"/"+currentNode.name, 120, "...")) - } + log.Infof("Node: %s\n", formatHierarchyPath(pathPrefix, currentNode.name, currentNode.nodeType, printNodesIfCN)) // iterate through children pathPrefix = pathPrefix + "/" + currentNode.name diff --git a/ldap_test.go b/ldap_test.go index 7ce48b0..6eb0e6e 100644 --- a/ldap_test.go +++ b/ldap_test.go @@ -1,7 +1,10 @@ package authaus import ( + "github.com/IMQS/log" + "github.com/go-ldap/ldap/v3" "github.com/stretchr/testify/assert" + "strings" "testing" "time" ) @@ -135,3 +138,233 @@ func Test_printTrunc(t *testing.T) { }) } } + +func Test_extractPath(t *testing.T) { + tests := []struct { + name string + dn string + want []pathElement + }{ + { + name: "empty DN", + dn: "", + want: []pathElement{}, + }, + { + name: "single DC component", + dn: "DC=com", + want: []pathElement{{name: "com", nodeType: "DC"}}, + }, + { + name: "normal DN", + dn: "CN=John Doe,OU=Users,DC=example,DC=com", + want: []pathElement{ + {name: "com", nodeType: "DC"}, + {name: "example", nodeType: "DC"}, + {name: "Users", nodeType: "OU"}, + {name: "John Doe", nodeType: "CN"}, + }, + }, + { + name: "DN with spaces around separators", + dn: "CN=Jane , OU=Staff , DC=corp , DC=org", + want: []pathElement{ + {name: "org", nodeType: "DC"}, + {name: "corp", nodeType: "DC"}, + {name: "Staff", nodeType: "OU"}, + {name: "Jane", nodeType: "CN"}, + }, + }, + { + name: "malformed element with no equals sign", + dn: "nodns", + want: []pathElement{}, + }, + { + name: "element with equals at end (empty value) is filtered out", + dn: "DC=com,OU=", + want: []pathElement{{name: "com", nodeType: "DC"}}, + }, + { + name: "value containing an equals sign", + dn: "CN=John=Doe,DC=com", + want: []pathElement{ + {name: "com", nodeType: "DC"}, + {name: "John=Doe", nodeType: "CN"}, + }, + }, + { + name: "mixed valid and malformed elements", + dn: "CN=Alice,badpart,DC=example", + want: []pathElement{ + {name: "example", nodeType: "DC"}, + {name: "Alice", nodeType: "CN"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractPath(tt.dn) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_extractHierarchy(t *testing.T) { + t.Run("empty search result", func(t *testing.T) { + sr := &ldap.SearchResult{} + root := extractHierarchy(sr) + assert.NotNil(t, root) + assert.Equal(t, "ROOT", root.name) + assert.Equal(t, "ROOT", root.nodeType) + assert.Empty(t, root.children) + }) + + t.Run("single entry builds correct tree", func(t *testing.T) { + sr := &ldap.SearchResult{ + Entries: []*ldap.Entry{ + {DN: "CN=John Doe,OU=Users,DC=example,DC=com"}, + }, + } + root := extractHierarchy(sr) + assert.NotNil(t, root) + + // Expect: ROOT → com → example → Users → John Doe + comNode, ok := root.children["com"] + assert.True(t, ok, "expected 'com' child under ROOT") + assert.Equal(t, "DC", comNode.nodeType) + + exampleNode, ok := comNode.children["example"] + assert.True(t, ok, "expected 'example' child under 'com'") + assert.Equal(t, "DC", exampleNode.nodeType) + + usersNode, ok := exampleNode.children["Users"] + assert.True(t, ok, "expected 'Users' child under 'example'") + assert.Equal(t, "OU", usersNode.nodeType) + + johnNode, ok := usersNode.children["John Doe"] + assert.True(t, ok, "expected 'John Doe' child under 'Users'") + assert.Equal(t, "CN", johnNode.nodeType) + assert.Empty(t, johnNode.children) + }) + + t.Run("multiple entries share common path components", func(t *testing.T) { + sr := &ldap.SearchResult{ + Entries: []*ldap.Entry{ + {DN: "CN=Alice,OU=Users,DC=example,DC=com"}, + {DN: "CN=Bob,OU=Users,DC=example,DC=com"}, + {DN: "CN=Carol,OU=Admins,DC=example,DC=com"}, + }, + } + root := extractHierarchy(sr) + + comNode := root.children["com"] + assert.NotNil(t, comNode) + exampleNode := comNode.children["example"] + assert.NotNil(t, exampleNode) + + // Both OUs should be present + assert.Len(t, exampleNode.children, 2) + + usersNode := exampleNode.children["Users"] + assert.NotNil(t, usersNode) + assert.Contains(t, usersNode.children, "Alice") + assert.Contains(t, usersNode.children, "Bob") + + adminsNode := exampleNode.children["Admins"] + assert.NotNil(t, adminsNode) + assert.Contains(t, adminsNode.children, "Carol") + }) + + t.Run("entry with malformed DN produces no children", func(t *testing.T) { + sr := &ldap.SearchResult{ + Entries: []*ldap.Entry{ + {DN: "nodns"}, + }, + } + root := extractHierarchy(sr) + assert.NotNil(t, root) + assert.Empty(t, root.children) + }) +} + +func Test_printHierarchy(t *testing.T) { + logger := log.NewTesting(t) + + t.Run("nil node does not panic", func(t *testing.T) { + // This test checks that calling printHierarchy with a nil node does not panic. + assert.NotPanics(t, func() { + printHierarchy(nil, "", false, logger) + }) + // No output is asserted, as formatHierarchyPath is not called for nil. + }) + + t.Run("single node with OU type formats slash-separated path", func(t *testing.T) { + logger := log.NewTesting(t) + node := &hierarchyNode{ + name: "Users", + nodeType: "OU", + children: make(map[string]*hierarchyNode), + } + assert.NotPanics(t, func() { + printHierarchy(node, "ROOT", false, logger) + }) + path := formatHierarchyPath("ROOT", node.name, node.nodeType, false) + assert.Equal(t, "ROOT/Users", path) + }) + + t.Run("CN node with printNodesIfCN=true formats colon-separated path", func(t *testing.T) { + node := &hierarchyNode{ + name: "John Doe", + nodeType: "CN", + children: make(map[string]*hierarchyNode), + } + assert.NotPanics(t, func() { + printHierarchy(node, "ROOT/example/Users", true, logger) + }) + path := formatHierarchyPath("ROOT/example/Users", node.name, node.nodeType, true) + assert.Equal(t, "ROOT/example/Users : John Doe", path) + }) + + t.Run("tree with children recurses without panic", func(t *testing.T) { + child := &hierarchyNode{ + name: "John Doe", + nodeType: "CN", + children: make(map[string]*hierarchyNode), + } + parent := &hierarchyNode{ + name: "Users", + nodeType: "OU", + children: map[string]*hierarchyNode{"John Doe": child}, + } + assert.NotPanics(t, func() { + printHierarchy(parent, "ROOT", false, logger) + }) + }) +} + +func Test_formatHierarchyPath(t *testing.T) { + t.Run("OU node returns slash-separated path", func(t *testing.T) { + got := formatHierarchyPath("ROOT", "Users", "OU", false) + assert.Contains(t, got, "ROOT/Users") + }) + + t.Run("CN node with printNodesIfCN=false returns slash-separated path", func(t *testing.T) { + got := formatHierarchyPath("ROOT/Users", "John Doe", "CN", false) + assert.Contains(t, got, "ROOT/Users/John Doe") + }) + + t.Run("CN node with printNodesIfCN=true returns colon-separated path", func(t *testing.T) { + got := formatHierarchyPath("ROOT/Users", "John Doe", "CN", true) + assert.Contains(t, got, "ROOT/Users : John Doe") + }) + + // Ensure the generated path exceeds 120 characters for truncation + t.Run("truncation works for long paths", func(t *testing.T) { + prefix := strings.Repeat("a", 130) + longNode := strings.Repeat("b", 20) + got := formatHierarchyPath(prefix, longNode, "OU", false) + assert.True(t, len(got) <= 120, "Output should be truncated to 120 characters or less") + assert.True(t, strings.HasSuffix(got, "..."), "Output should end with '...'") + }) +}