Modernize tests, reduce execution time from 30s to 0.2s

Make output go to test logger so that go test -v will give useful output on a failure
Remove dependence on `oathtool`
Remove dependence on `ls`
Wait on killing the running glauth instance so subsequent tests don't fail
Wait for glauth to provide authentication error instead of waiting an arbitrary amount of time
This commit is contained in:
Timmy Welch
2026-08-05 16:16:10 -07:00
parent 26cd4447c8
commit 319cc9a3c7
4 changed files with 182 additions and 66 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
go 1.24.0
go 1.25.0
use ./v2
+178 -64
View File
@@ -1,8 +1,9 @@
package main
import (
"bytes"
"errors"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
@@ -10,6 +11,9 @@ import (
"strings"
"testing"
"time"
"github.com/glauth/ldap"
"github.com/pquerna/otp/totp"
)
type testEnv struct {
@@ -60,12 +64,7 @@ func TestProperBuild(t *testing.T) {
}
func TestConfigBackendRejectsPasswordlessUserBinds(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := listener.Addr().(*net.TCPAddr).Port
listener.Close()
port := 3893
configPath := filepath.Join(t.TempDir(), "passwordless.cfg")
config := fmt.Sprintf(`debug = false
@@ -97,11 +96,11 @@ func TestConfigBackendRejectsPasswordlessUserBinds(t *testing.T) {
action = "search"
object = "*"
`, port)
if err := os.WriteFile(configPath, []byte(config), 0600); err != nil {
if err := os.WriteFile(configPath, []byte(config), 0o600); err != nil {
t.Fatal(err)
}
svc := startSvc(SD, glauthBinary(), "-c", configPath)
svc := startSvc(t, glauthBinary(), "-c", configPath)
defer stopSvc(svc)
bindDN := "cn=nopass,dc=glauth,dc=com"
@@ -114,7 +113,11 @@ func TestConfigBackendRejectsPasswordlessUserBinds(t *testing.T) {
{name: "empty password", password: "", want: "exit status 53"},
} {
t.Run(tc.name, func(t *testing.T) {
got := doRunGetFirst(0, "ldapsearch", "-LLL", "-H", fmt.Sprintf("ldap://127.0.0.1:%d", port), "-D", bindDN, "-w", tc.password, "-x", "-b", "dc=glauth,dc=com", "(cn=nopass)")
out, err := doRun("ldapsearch", "-LLL", "-H", fmt.Sprintf("ldap://127.0.0.1:%d", port), "-D", bindDN, "-w", tc.password, "-x", "-b", "dc=glauth,dc=com", "(cn=nopass)")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != tc.want {
t.Fatalf("bind result = %q, want %q", got, tc.want)
}
@@ -149,7 +152,7 @@ func ldapOnlyConfig(t *testing.T, source string) string {
contents = contents[:start] + sectionWithoutLDAPS + contents[end:]
path := filepath.Join(t.TempDir(), filepath.Base(source))
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return path
@@ -164,8 +167,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
{
Name: "searching for the 'hackers' user",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
if got, want := out, env.expectedaccount; got != want {
want := env.expectedaccount
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
@@ -176,8 +185,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
if env.svcdnnogroup == "" {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdnnogroup, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
if got, want := out, env.expectedaccount; got != want {
want := env.expectedaccount
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdnnogroup, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
@@ -188,8 +203,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
if !env.checkbindUPN {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", "serviceuser@example.com", "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
if got, want := out, env.expectedaccount; got != want {
want := env.expectedaccount
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", "serviceuser@example.com", "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
@@ -197,8 +218,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
{
Name: "querying the root SDE",
Check: func(t testing.TB) {
out := doRunGetSecond(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-s", "base", "(objectclass=*)")
if got, want := out, env.expectedinfo; got != want {
want := env.expectedinfo
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-s", "base", "(objectclass=*)")
got := getSecond(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should get some meta information\ngot: %s\nwant: %s", got, want)
}
},
@@ -209,8 +236,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
if !env.checkanonymousrootDSE {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-x", "-s", "base", "(objectclass=*)")
if got, want := out, "exit status 50"; got != want {
want := "exit status 50"
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-x", "-s", "base", "(objectclass=*)")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should get error 50\ngot: %s\nwant: %s", got, want)
}
},
@@ -218,8 +251,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
{
Name: "enumerating posix groups",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(objectclass=posixgroup)")
if got, want := out, env.expectedgroup; got != want {
want := env.expectedgroup
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(objectclass=posixgroup)")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should get a list starting with the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
@@ -227,8 +266,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
{
Name: "searching for members of the 'superheros' group",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com)")
if got, want := out, env.expectedfirstaccount; got != want {
want := env.expectedfirstaccount
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com)")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want)
}
},
@@ -236,8 +281,13 @@ func batteryOfTests(t *testing.T, env *testEnv) {
{
Name: "performing a complex search for members of 'superheros' group",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(&(objectClass=*)(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com))")
if got, want := out, env.expectedfirstaccount; got != want {
want := env.expectedfirstaccount
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(&(objectClass=*)(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com))")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want)
}
},
@@ -248,9 +298,21 @@ func batteryOfTests(t *testing.T, env *testEnv) {
if !env.checkTOTP {
t.SkipNow()
}
otpvalue := doRunGetFirst(RD, "oathtool", "--totp", "-b", "-d", "6", "3hnvnk4ycv44glzigd6s25j4dougs3rk")
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret"+otpvalue, "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
if got, want := out, env.scopedaccount; got != want {
// We aren't checking that "github.com/pquerna/otp/totp" creates correct values.
// That's what their own tests are for.
// We are checking that the totp code gets through glauth code successfully
otpvalue, err := totp.GenerateCode("3hnvnk4ycv44glzigd6s25j4dougs3rk", time.Now())
if err != nil {
t.Fatal("Failed to generate totp code:", err)
}
want := env.scopedaccount
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.otpdn, "-w", "mysecret"+otpvalue, "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
got := getFirst(out)
if got != want {
if err != nil {
got = err.Error()
}
t.Log(out)
t.Fatalf("should find them in in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
@@ -261,8 +323,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
if !env.checkTOTP {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
if got, want := out, "exit status 49"; got != want {
want := "exit status 49"
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.otpdn, "-w", "mysecret", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want)
}
},
@@ -273,8 +341,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
if !env.checkTOTP {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret123456", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
if got, want := out, "exit status 49"; got != want {
want := "exit status 49"
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.otpdn, "-w", "mysecret123456", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
got := getFirst(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want)
}
},
@@ -285,8 +359,14 @@ func batteryOfTests(t *testing.T, env *testEnv) {
if env.checkemployeetype == "" {
t.SkipNow()
}
out := doRunGetSecond(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", env.checkemployeetype, "employeetype")
if got, want := out, "employeetype: Intern"; got != want {
want := "employeetype: Intern"
out, err := doRun("ldapsearch", "-LLL", "-H", "ldap://127.0.0.1:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", env.checkemployeetype, "employeetype")
got := getSecond(out)
if err != nil {
got = err.Error()
}
if got != want {
t.Log(out)
t.Fatalf("type should be 'Intern'\ngot: %s\nwant: %s", got, want)
}
},
@@ -316,14 +396,15 @@ func TestSampleSimple(t *testing.T) {
checkemployeetype: "cn=hackers",
}
svc := startSvc(SD, glauthBinary(), "-c", ldapOnlyConfig(t, "sample-simple.cfg"))
svc := startSvc(t, glauthBinary(), "-c", ldapOnlyConfig(t, "sample-simple.cfg"))
batteryOfTests(t, &env)
stopSvc(svc)
}
func TestSQLitePlugin(t *testing.T) {
matchingLibrary := doRunGetFirst(RD, "ls", "bin/sqlite.so")
if matchingLibrary != "bin/sqlite.so" {
_, err := os.Stat("bin/sqlite.so")
if err != nil {
t.Log("Unable to find sqlite plugin")
t.SkipNow()
}
@@ -342,14 +423,20 @@ func TestSQLitePlugin(t *testing.T) {
checkemployeetype: "",
}
svc := startSvc(SD, glauthBinary(), "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg")
svc := startSvc(t, glauthBinary(), "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg")
batteryOfTests(t, &env)
stopSvc(svc)
}
func TestLdapInjection(t *testing.T) {
matchingContainers := doRunGetFirst(RD, "sh", "-c", "docker ps | grep ldap-service | wc -l")
out, err := doRun("sh", "-c", "docker ps | grep ldap-service | wc -l")
if err != nil {
t.Log("Unable to find docker container:", out)
t.SkipNow()
}
matchingContainers := getFirst(out)
if matchingContainers != "1" {
t.Log("Unable to find docker container:", out)
t.SkipNow()
}
@@ -368,52 +455,79 @@ func TestLdapInjection(t *testing.T) {
checkemployeetype: "",
}
svc := startSvc(SD, glauthBinary(), "-c", "sample-ldap-injection.cfg")
svc := startSvc(t, glauthBinary(), "-c", "sample-ldap-injection.cfg")
batteryOfTests(t, &env)
stopSvc(svc)
}
// -----=============================================================================----
const SD = 3 // Start Delay
const RD = 2 // Response Delay
func startSvc(delay time.Duration, name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
cmd.Start()
if delay != 0 {
time.Sleep(time.Second * delay)
func WaitForPort(host string, timeout time.Duration) error {
start := time.Now()
for {
conn, err := ldap.Dial("tcp", host)
if err == nil {
err = conn.Bind("_", "_")
if err == nil {
conn.Close()
return fmt.Errorf("Successfully authenticated to glauth. This should not happen")
}
e := &ldap.Error{}
if errors.As(err, &e) && e.ResultCode == 49 {
conn.Close()
return nil
}
conn.Close()
// We have an error, it's probably a connection error or an error saying that ldap isn't setup yet
// try again
}
if time.Since(start) > timeout {
return fmt.Errorf("timeout waiting for port %s: %w", host, err)
}
time.Sleep(50 * time.Millisecond)
}
}
func startSvc(t *testing.T, name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
cmd.Stderr = t.Output()
cmd.Stdout = t.Output()
cmd.Start()
err := WaitForPort("127.0.0.1:3893", time.Second*2)
if err != nil {
t.Fatal("Failed to wait for glauth to start:", err)
}
// the port is listening but the ldap server may not be accepting connections yet
// TODO: make glauth listen as the last part of the initialization that way as soon as it listens it can process connections
return cmd
}
func stopSvc(svc *exec.Cmd) {
svc.Process.Kill()
svc.Process.Wait()
}
func doRunGetFirst(delay time.Duration, name string, arg ...string) string {
out := strings.SplitN(doRun(delay, name, arg...), "\n", 2)
if out == nil || len(out) < 1 {
func getFirst(output string) string {
out := strings.SplitN(output, "\n", 2)
if len(out) < 1 {
return "*fail*"
}
return out[0]
}
func doRunGetSecond(delay time.Duration, name string, arg ...string) string {
out := strings.SplitN(doRun(delay, name, arg...), "\n", 3)
if out == nil || len(out) < 2 {
func getSecond(output string) string {
out := strings.SplitN(output, "\n", 3)
if len(out) < 2 {
return "*fail*"
}
return out[1]
}
func doRun(delay time.Duration, name string, arg ...string) string {
out, err := exec.Command(name, arg...).Output()
func doRun(name string, arg ...string) (string, error) {
out, err := exec.Command(name, arg...).CombinedOutput()
if err != nil {
return err.Error()
return string(bytes.TrimSpace(out)), err
}
if delay != 0 {
time.Sleep(time.Second * delay)
}
return strings.TrimSpace(string(out))
return string(bytes.TrimSpace(out)), nil
}
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/glauth/glauth/v2
go 1.21
go 1.25
require (
github.com/BurntSushi/toml v1.3.2
+2
View File
@@ -15,6 +15,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=