v0.12.0 migrate sqlite3 to psql18, updated auth, deploy docs, account and replay updates
test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-26 20:58:19 -04:00
1 parent 5d2be4abb2
commit d0326b059e
66 files changed
+4016 -2433

No files matched your search

+114 -14
View File
@@ -1,14 +1,21 @@
package service
import (
"errors"
"testing"
"time"
"chess/internal/server/storage"
"github.com/google/uuid"
"github.com/lixenwraith/auth"
)
func TestValidateTokenRequiresSessionBoundToSubject(t *testing.T) {
var testJWTSecret = []byte("test-secret-test-secret-test-secret")
func TestValidateTokenRequiresScopedTokenBoundToSession(t *testing.T) {
svc := newPersistentTestService(t)
user, sessionID, err := svc.RegisterUser("alice", "", "Password1", false)
user, sessionID, err := svc.RegisterUser("alice", "", "Password1")
if err != nil {
t.Fatal(err)
}
@@ -17,26 +24,47 @@ func TestValidateTokenRequiresSessionBoundToSubject(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if gotUserID, _, err := svc.ValidateToken(validToken); err != nil || gotUserID != user.UserID {
t.Fatalf("valid token rejected: user=%q err=%v", gotUserID, err)
gotUserID, claims, err := svc.ValidateToken(validToken)
if err != nil || gotUserID != user.UserID || claims["session_id"] != sessionID {
t.Fatalf("valid token rejected: user=%q claims=%v err=%v", gotUserID, claims, err)
}
missingSession, err := auth.GenerateHS256Token(svc.jwtSecret, user.UserID, nil, SessionTTL)
scoped, err := auth.NewJWT(testJWTSecret,
auth.WithIssuer(JWTIssuer), auth.WithAudience([]string{JWTAudience}))
if err != nil {
t.Fatal(err)
}
if _, _, err := svc.ValidateToken(missingSession); err == nil {
t.Fatal("token without a persisted session was accepted")
}
wrongSubject, err := auth.GenerateHS256Token(svc.jwtSecret, "other-user", map[string]any{
"session_id": sessionID,
}, SessionTTL)
unscoped, err := auth.NewJWT(testJWTSecret)
if err != nil {
t.Fatal(err)
}
if _, _, err := svc.ValidateToken(wrongSubject); err == nil {
t.Fatal("session was accepted for a different JWT subject")
foreignAudience, err := auth.NewJWT(testJWTSecret,
auth.WithIssuer(JWTIssuer), auth.WithAudience([]string{"other-service"}))
if err != nil {
t.Fatal(err)
}
for name, mint := range map[string]func() (string, error){
"missing session": func() (string, error) { return scoped.GenerateToken(user.UserID, nil) },
"wrong subject": func() (string, error) {
return scoped.GenerateToken(uuid.NewString(), map[string]any{"session_id": sessionID})
},
"malformed subject": func() (string, error) {
return scoped.GenerateToken("other-user", map[string]any{"session_id": sessionID})
},
"no issuer or audience": func() (string, error) {
return unscoped.GenerateToken(user.UserID, map[string]any{"session_id": sessionID})
},
"foreign audience": func() (string, error) {
return foreignAudience.GenerateToken(user.UserID, map[string]any{"session_id": sessionID})
},
} {
token, err := mint()
if err != nil {
t.Fatal(err)
}
if _, _, err := svc.ValidateToken(token); err == nil {
t.Errorf("%s: token accepted", name)
}
}
if err := svc.InvalidateSession(sessionID); err != nil {
@@ -46,3 +74,75 @@ func TestValidateTokenRequiresSessionBoundToSubject(t *testing.T) {
t.Fatal("invalidated session was accepted")
}
}
func TestAuthenticateUserFailuresAreIndistinguishable(t *testing.T) {
svc := newPersistentTestService(t)
if _, _, err := svc.RegisterUser("alice", "alice@example.com", "Password1"); err != nil {
t.Fatal(err)
}
if _, _, err := svc.RegisterUser("ALICE", "", "Password1"); !errors.Is(err, ErrUserExists) {
t.Fatalf("duplicate registration = %v, want ErrUserExists", err)
}
user, sessionID, err := svc.AuthenticateUser("alice@example.com", "Password1")
if err != nil || sessionID == "" || user.Username != "alice" {
t.Fatalf("login by email = %+v, %q, %v", user, sessionID, err)
}
for name, attempt := range map[string][2]string{
"unknown user": {"nobody", "Password1"},
"unknown email": {"nobody@example.com", "Password1"},
"wrong password": {"alice", "Password2"},
} {
if _, _, err := svc.AuthenticateUser(attempt[0], attempt[1]); !errors.Is(err, ErrInvalidCredentials) {
t.Errorf("%s: error = %v, want ErrInvalidCredentials", name, err)
}
}
}
func TestRegistrationCapIsConfigurable(t *testing.T) {
svc := newPersistentTestService(t)
svc.SetMaxUsers(1)
if _, _, err := svc.RegisterUser("alice", "", "Password1"); err != nil {
t.Fatal(err)
}
if _, _, err := svc.RegisterUser("bob", "", "Password1"); !errors.Is(err, ErrAtCapacity) {
t.Fatalf("registration past the cap = %v, want ErrAtCapacity", err)
}
svc.SetMaxUsers(0)
if _, _, err := svc.RegisterUser("bob", "", "Password1"); err != nil {
t.Fatalf("registration without a cap: %v", err)
}
}
func TestPasswordHashingIsBounded(t *testing.T) {
svc, err := New(nil, testJWTSecret)
if err != nil {
t.Fatal(err)
}
var releases []func()
for range MaxConcurrentKDF {
release, err := svc.acquireKDF()
if err != nil {
t.Fatal(err)
}
releases = append(releases, release)
}
started := time.Now()
if _, err := svc.hashPassword("Password1"); !errors.Is(err, ErrAuthBusy) {
t.Fatalf("saturated KDF = %v, want ErrAuthBusy", err)
}
if waited := time.Since(started); waited < kdfWaitTimeout {
t.Fatalf("gave up after %v, want %v", waited, kdfWaitTimeout)
}
releases[0]()
if _, err := svc.hashPassword("Password1"); err != nil {
t.Fatalf("hash after release: %v", err)
}
}
func TestNewRejectsShortJWTSecret(t *testing.T) {
if _, err := New((*storage.Store)(nil), make([]byte, 31)); err == nil {
t.Fatal("31-byte JWT secret accepted")
}
}