Skip to content
Golang bindings for a dockerized test LDAP server
Go
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
pkg/testslapd
.gitignore
LICENSE
README.md
go.mod
go.sum

README.md

go-ldap-test

A library for testing code against an LDAP directory.

To the author's knowledge, there does not exist an in-memory LDAP server compatible with OpenLDAP for Golang. There exist plenty of quality libraries that almost fit the author's needs, but not quite.

Since time was limited, the author did the next best thing and wrote some code to do stand up an LDAP directory via docker.

It's less elegant than a pure code solution would be, but it works, and it allows one to test one's LDAP dependent code against a temporary local directory server- which was the goal.

In your test code, set up something like this:

import "github.com/scribd/go-testslapd/pkg/testslapd"

var slapd *testslapd.TestSlapd
var ldapSetup bool

func TestMain(m *testing.M) {
    setUp()

    code := m.Run()

    tearDown()

    os.Exit(code)
}

// runs before the tests start
func setUp() {
    // start the test server if it's not already running
    if !ldapSetup {
        port, err := freeport.GetFreePort()
        if err != nil {
            fmt.Printf("Error getting free port: %s\n", err)
            os.Exit(1)

        }

        log.Printf("Starting LDAP Server on port %d", port)
        
        org := "scribd"
        base := "dc=scribd,dc=com"
        domain := "scribd.com"
        adminPassword := "letmein"
        bindDn := fmt.Sprintf("cn=admin,dc=scribd,dc=com")

        slapd = testslapd.NewTestSlapd(port, org, base, domain, adminPassword, "", "")

        slapd.SetVerbose(true)

        slapd.SetProvisioner(func() error {
            l, err := ldap.Dial("tcp", slapd.Address)
            if err != nil {
                fmt.Printf("Failed to dial LDAP at %s: %s", slapd.Address, err)
                t.Fail()
            }

            err = l.Bind(bindDn, adminPassword)
            if err != nil {
                log.Printf("--- failed to bind to ldap: %s ---", err)
                return err
            }

            fmt.Printf("--- Running provision function ---\n")

            log.Printf("--- adding group ou ---")
            r := ldap.NewAddRequest("ou=group,dc=scribd,dc=com")
            r.Attribute("ou", []string{"group"})
            r.Attribute("objectClass", []string{"top", "organizationalUnit"})

            err = l.Add(r)
            if err != nil {
                log.Printf("failed to add group ou to directory: %s", err)
                t.Fail()
            }

            r = ldap.NewAddRequest("ou=users,dc=scribd,dc=com")
            r.Attribute("ou", []string{"users"})
            r.Attribute("objectClass", []string{"top", "organizationalUnit"})

            log.Printf("--- adding users ou ---")

            err = l.Add(r)
            if err != nil {
                log.Printf("failed to add users ou to directory: %s", err)
                t.Fail()
            }

            l.Close()

            fmt.Printf("--- End provision function ---\n")

            return err
        })

        err = slapd.StartTestServer()
        if err != nil {
            fmt.Printf("Failed starting slapd container %q: %s", slapd.ContainerName, err)
            log.Fatal(err)
        }

        err = slapd.Provision()
        if err != nil {
            fmt.Printf("Failed running provisioner: %s", err)
            log.Fatal(err)
        }

        ldapSetup = true
    }
}

// runs at the end to clean up
func tearDown() {
    err := slapd.StopTestServer()
    if err != nil {
        log.Fatalf(err.Error())
    }
}

// run a test that connects to the server
func TestSomething(t *testing.T)  {
    l, err := ldap.Dial("tcp", slapd.Address)
    if err != nil {
        fmt.Printf("Failed to dial LDAP at %s: %s", slapd.Address, err)
        t.Fail()
    }

    err = l.Bind(bindDn, adminPassword)
    if err != nil {
        log.Printf("--- failed to bind to ldap: %s ---", err)
        t.Fail()
    }

    req := ldap.NewSearchRequest(
        base,
        ldap.ScopeWholeSubtree,
        ldap.NeverDerefAliases,
        0,
        0,
        false,
        "(objectClass=top)",
            []string{},
        nil,
        )

    resp, err := l.Search(req)
    if err != nil {
        fmt.Printf("Failed searching directory: %s", err)
        t.Fail()
    }
    assert.True(t, len(resp.Entries) == 4, "Expected entries in directory")

    inputs := []struct{
        name string
        dn string
    }{
        {
            "org",
            "dc=scribd,dc=com",
        },
        {
            "admin",
            "cn=admin,dc=scribd,dc=com",
        },
        {
            "group",
            "ou=group,dc=scribd,dc=com",
        },
        {
            "users",
            "ou=users,dc=scribd,dc=com",
        },
    }

    for i, tc := range inputs {
        t.Run(tc.name, func(t *testing.T){
            assert.True(t, resp.Entries[i].DN == tc.dn, "Missing expected DN in directory")
        })
    }

    l.Close()
}

Caveats

  • As currently written, only 1 container can run at a time. Concurrent tests of multiple projects using this library might encounter name collision on the test container name.

  • If your tests panic, you will likely need to clean out the test container manualy via docker rm -f ldaptest. Anything that causes golang to simply stop without running the cleanup code will leave the container running in the background.

  • This package does not install docker for you. The docker binary is assumed to be already present, in the PATH, and that the user running the tests has access to the docker daemon.

You can’t perform that action at this time.