Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
233 changes: 233 additions & 0 deletions ldap_test.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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 '...'")
})
}