1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-10-28 15:02:28 +00:00
Files
go-chasquid-smtp/internal/auth/auth_test.go
T
Alberto Bertogli 8b19760f4e test: Use modern testing features instead of custom helpers
Many of our tests rely on helpers that predate features which are now
available in the standard library.

This patch replaces them with the standard equivalents:

- MustTempDir/RemoveIfOk -> t.ArtifactDir() or t.TempDir() (depending on
  whether it is useful to keep the data on request).
- GetFreePort + waitForServer -> opening the listeners before starting
  the servers. To support this, localrpc.Server gains a Serve method. We
  may re-introduce these in the future if more use cases appear.
- WaitFor and real-time timing checks -> testing/synctest.
- context.Background() -> t.Context().
- Benchmark b.N loops -> b.Loop().
- reflect.DeepEqual -> cmp.Diff, and proto.Equal for protobufs.
2026-10-03 10:34:35 +01:00

292 lines
7.2 KiB
Go

package auth
import (
"encoding/base64"
"fmt"
"testing"
"testing/synctest"
"time"
"blitiri.com.ar/go/chasquid/internal/dovecot"
"blitiri.com.ar/go/chasquid/internal/trace"
"blitiri.com.ar/go/chasquid/internal/userdb"
)
func TestDecodeResponse(t *testing.T) {
// Successful cases. Note we hard-code the response for extra assurance.
cases := []struct {
response, user, domain, passwd string
}{
{"dUBkAHVAZABwYXNz", "u", "d", "pass"}, // u@d\0u@d\0pass
{"dUBkAABwYXNz", "u", "d", "pass"}, // u@d\0\0pass
{"AHVAZABwYXNz", "u", "d", "pass"}, // \0u@d\0pass
{"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff
{"dQB1AHBhc3M=", "u", "", "pass"}, // u\0u\0pass
// "ñaca@ñeque\0\0clavaré"
{"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"},
}
for _, c := range cases {
u, d, p, err := DecodeResponse(c.response)
if err != nil {
t.Errorf("Error in case %v: %v", c, err)
}
if u != c.user || d != c.domain || p != c.passwd {
t.Errorf("Expected %q %q %q ; got %q %q %q",
c.user, c.domain, c.passwd, u, d, p)
}
}
_, _, _, err := DecodeResponse("this is not base64 encoded")
if err == nil {
t.Errorf("invalid base64 did not fail as expected")
}
failedCases := []string{
"", "\x00", "\x00\x00", "\x00\x00\x00", "\x00\x00\x00\x00",
"a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass",
"\xffa@b\x00\xffa@b\x00pass",
}
for _, c := range failedCases {
r := base64.StdEncoding.EncodeToString([]byte(c))
_, _, _, err := DecodeResponse(r)
if err == nil {
t.Errorf("Expected case %q to fail, but succeeded", c)
} else {
t.Logf("OK: %q failed with %v", c, err)
}
}
}
func TestAuthenticate(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
db := userdb.New("/dev/null")
db.AddUser("user", "password")
tr := trace.New("test", "TestAuthenticate")
defer tr.Finish()
a := NewAuthenticator()
a.Register("domain", WrapNoErrorBackend(db))
// Test the correct case first
check(t, a, "user", "domain", "password", true)
// Wrong password, but valid user@domain.
ts := time.Now()
if ok, _ := a.Authenticate(tr, "user", "domain", "invalid"); ok {
t.Errorf("invalid password, but authentication succeeded")
}
if time.Since(ts) < a.AuthDuration {
t.Errorf("authentication was too fast (invalid case)")
}
// Incorrect cases, where the user@domain do not exist.
cases := []struct{ user, domain, password string }{
{"user", "unknown", "password"},
{"invalid", "domain", "p"},
{"invalid", "unknown", "p"},
{"user", "", "password"},
{"invalid", "", "p"},
{"", "domain", "password"},
{"", "", ""},
}
for _, c := range cases {
check(t, a, c.user, c.domain, c.password, false)
}
})
}
func check(t *testing.T, a *Authenticator, user, domain, passwd string, expect bool) {
c := fmt.Sprintf("{%s@%s %s}", user, domain, passwd)
ts := time.Now()
tr := trace.New("test", "check")
defer tr.Finish()
ok, err := a.Authenticate(tr, user, domain, passwd)
if time.Since(ts) < a.AuthDuration {
t.Errorf("auth on %v was too fast", c)
}
if ok != expect {
t.Errorf("auth on %v: got %v, expected %v", c, ok, expect)
}
if err != nil {
t.Errorf("auth on %v: got error %v", c, err)
}
ok, err = a.Exists(tr, user, domain)
if ok != expect {
t.Errorf("exists on %v: got %v, expected %v", c, ok, expect)
}
if err != nil {
t.Errorf("exists on %v: error %v", c, err)
}
}
func TestInterfaces(t *testing.T) {
var _ NoErrorBackend = userdb.New("/dev/null")
var _ Backend = dovecot.NewAuth("/dev/null", "/dev/null")
}
// Backend implementation for testing.
type TestBE struct {
users map[string]string
reloadCount int
nextError error
}
func NewTestBE() *TestBE {
return &TestBE{
users: map[string]string{},
}
}
func (d *TestBE) add(user, password string) {
d.users[user] = password
}
func (d *TestBE) Authenticate(user, password string) (bool, error) {
if d.nextError != nil {
return false, d.nextError
}
if validP, ok := d.users[user]; ok {
return validP == password, nil
}
return false, nil
}
func (d *TestBE) Exists(user string) (bool, error) {
if d.nextError != nil {
return false, d.nextError
}
_, ok := d.users[user]
return ok, nil
}
func (d *TestBE) Reload() error {
d.reloadCount++
if d.nextError != nil {
return d.nextError
}
return nil
}
func TestMultipleBackends(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
domain1 := NewTestBE()
domain2 := NewTestBE()
fallback := NewTestBE()
a := NewAuthenticator()
a.Register("domain1", domain1)
a.Register("domain2", domain2)
a.Fallback = fallback
domain1.add("user1", "passwd1")
domain2.add("user2", "passwd2")
fallback.add("user3@fallback", "passwd3")
fallback.add("user4@domain1", "passwd4")
// Successful tests.
cases := []struct{ user, domain, password string }{
{"user1", "domain1", "passwd1"},
{"user2", "domain2", "passwd2"},
{"user3", "fallback", "passwd3"},
{"user4", "domain1", "passwd4"},
}
for _, c := range cases {
check(t, a, c.user, c.domain, c.password, true)
}
// Unsuccessful tests (users don't exist).
cases = []struct{ user, domain, password string }{
{"nobody", "domain1", "p"},
{"nobody", "domain2", "p"},
{"nobody", "fallback", "p"},
{"user3", "", "p"},
}
for _, c := range cases {
check(t, a, c.user, c.domain, c.password, false)
}
})
}
func TestErrors(t *testing.T) {
be := NewTestBE()
be.add("user", "passwd")
a := NewAuthenticator()
a.Register("domain", be)
a.AuthDuration = 0
tr := trace.New("test", "TestErrors")
defer tr.Finish()
ok, err := a.Authenticate(tr, "user", "domain", "passwd")
if err != nil || !ok {
t.Fatalf("failed auth")
}
expectedErr := fmt.Errorf("test error")
be.nextError = expectedErr
ok, err = a.Authenticate(tr, "user", "domain", "passwd")
if ok {
t.Errorf("authentication succeeded, expected error")
}
if err != expectedErr {
t.Errorf("expected error, got %v", err)
}
ok, err = a.Exists(tr, "user", "domain")
if ok {
t.Errorf("exists succeeded, expected error")
}
if err != expectedErr {
t.Errorf("expected error, got %v", err)
}
}
func TestReload(t *testing.T) {
be1 := NewTestBE()
be2 := NewTestBE()
fallback := NewTestBE()
a := NewAuthenticator()
a.Register("domain1", be1)
a.Register("domain2", be2)
a.Fallback = fallback
err := a.Reload()
if err != nil {
t.Errorf("unexpected error reloading: %v", err)
}
if be1.reloadCount != 1 || be2.reloadCount != 1 || fallback.reloadCount != 1 {
t.Errorf("unexpected reload counts: %d %d %d != 1 1 1",
be1.reloadCount, be2.reloadCount, fallback.reloadCount)
}
be2.nextError = fmt.Errorf("test error")
err = a.Reload()
if err == nil {
t.Errorf("expected error reloading, got nil")
}
if be1.reloadCount != 2 || be2.reloadCount != 2 || fallback.reloadCount != 2 {
t.Errorf("unexpected reload counts: %d %d %d != 2 2 2",
be1.reloadCount, be2.reloadCount, fallback.reloadCount)
}
a2 := NewAuthenticator()
a2.Register("domain", WrapNoErrorBackend(userdb.New("/dev/null")))
if err = a2.Reload(); err != nil {
t.Errorf("unexpected error reloading wrapped backend: %v", err)
}
}
// Fuzz testing for the response decoder, which handles user-provided data.
func FuzzDecodeResponse(f *testing.F) {
f.Fuzz(func(t *testing.T, response string) {
DecodeResponse(response)
})
}