package functional
import (
"html/template"
"net/url"
"os"
"sync"
"testing"
"time"
"github.com/coreos/dex/connector"
"golang.org/x/net/context"
"gopkg.in/ldap.v2"
)
type LDAPServer struct {
Host string // Address (host:port) of LDAP service.
LDAPSHost string // Address (host:port) of LDAPS service (TLS port).
BindDN string
BindPw string
}
const (
ldapEnvHost = "DEX_TEST_LDAP_HOST"
ldapsEnvHost = "DEX_TEST_LDAPS_HOST"
ldapEnvBindName = "DEX_TEST_LDAP_BINDNAME"
ldapEnvBindPass = "DEX_TEST_LDAP_BINDPASS"
)
func ldapServer(t *testing.T) LDAPServer {
getenv := func(key string) string {
val := os.Getenv(key)
if val == "" {
t.Fatalf("%s not set", key)
}
t.Logf("%s=%v", key, val)
return val
}
return LDAPServer{
Host: getenv(ldapEnvHost),
LDAPSHost: getenv(ldapsEnvHost),
BindDN: os.Getenv(ldapEnvBindName),
BindPw: os.Getenv(ldapEnvBindPass),
}
}
func TestLDAPConnect(t *testing.T) {
server := ldapServer(t)
l, err := ldap.Dial("tcp", server.Host)
if err != nil {
t.Fatal(err)
}
err = l.Bind(server.BindDN, server.BindPw)
if err != nil {
t.Fatal(err)
}
l.Close()
}
func TestConnectorLDAPHealthy(t *testing.T) {
server := ldapServer(t)
tests := []struct {
config connector.LDAPConnectorConfig
wantErr bool
}{
{
config: connector.LDAPConnectorConfig{
ID: "ldap",
Host: "localhost:0",
},
wantErr: true,
},
{
config: connector.LDAPConnectorConfig{
ID: "ldap",
Host: server.Host,
},
},
{
config: connector.LDAPConnectorConfig{
ID: "ldap",
Host: server.Host,
UseTLS: true,
CertFile: "/tmp/ldap.crt",
KeyFile: "/tmp/ldap.key",
CaFile: "/tmp/openldap-ca.pem",
},
},
{
config: connector.LDAPConnectorConfig{
ID: "ldap",
Host: server.LDAPSHost,
UseSSL: true,
CertFile: "/tmp/ldap.crt",
KeyFile: "/tmp/ldap.key",
CaFile: "/tmp/openldap-ca.pem",
},
},
}
for i, tt := range tests {
templates := template.New(connector.LDAPLoginPageTemplateName)
c, err := tt.config.Connector(url.URL{}, nil, templates)
if err != nil {
t.Errorf("case %d: failed to create connector: %v", i, err)
continue
}
if err := c.Healthy(); err != nil {
if !tt.wantErr {
t.Errorf("case %d: Healthy() returned error: %v", i, err)
}
} else if tt.wantErr {
t.Errorf("case %d: expected Healthy() to fail", i)
}
}
}
func TestLDAPPoolHighWatermarkAndLockContention(t *testing.T) {
server := ldapServer(t)
ldapPool := &connector.LDAPPool{
MaxIdleConn: 30,
Host: server.Host,
UseTLS: false,
UseSSL: false,
}
// Excercise pool operations with MaxIdleConn + 10 concurrent goroutines.
// We are testing both pool high watermark code and lock contention
numRoutines := ldapPool.MaxIdleConn + 10
var wg sync.WaitGroup
wg.Add(numRoutines)
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
for i := 0; i < numRoutines; i++ {
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
err := ldapPool.Do(func(conn *ldap.Conn) error {
s := &ldap.SearchRequest{
Scope: ldap.ScopeBaseObject,
Filter: "(objectClass=*)",
}
_, err := conn.Search(s)
return err
})
if err != nil {
t.Errorf("Search request failed. Dead/invalid LDAP connection from pool?: %v", err)
}
_, _ = ldapPool.CheckConnections()
}
}
}()
}
// Wait for all operations to complete and check status.
// There should be MaxIdleConn connections in the pool. This confirms:
// 1. The tests was indeed executed concurrently
// 2. Even though we ran more routines than the configured MaxIdleConn the high
// watermark code did its job and closed surplus connections
wg.Wait()
alive, killed := ldapPool.CheckConnections()
if alive < ldapPool.MaxIdleConn {
t.Errorf("expected %v connections, got alive=%v killed=%v", ldapPool.MaxIdleConn, alive, killed)
}
}