Files
synapbus/internal/auth/session_store_test.go
T
Algis DumbrisandClaude Opus 4.6 8a1c096355 feat: implement human auth with OAuth 2.1 (fosite)
Add complete auth subsystem with OAuth 2.1 authorization server using
ory/fosite, local user accounts with bcrypt password hashing, session
management, and HTTP handlers for the Web UI.

Components:
- User store with bcrypt hashing (configurable cost, default 12), CRUD,
  validation (username 3-64 chars alphanumeric+underscore, password 8-72 bytes)
- Session store with secure random IDs, configurable lifetime (default 24h),
  expiration cleanup, and per-user invalidation
- OAuth client store with client_id/secret generation and bcrypt verification
- Fosite storage adapter implementing CoreStorage, TokenRevocationStorage,
  and PKCERequestStorage backed by SQLite
- OAuth provider configured with authorization code (PKCE S256 mandatory),
  client credentials, refresh token rotation, and token introspection
- HTTP handlers: POST /auth/register, POST /auth/login, POST /auth/logout,
  GET /auth/me, PUT /auth/password, GET /oauth/authorize, POST /oauth/token,
  POST /oauth/introspect
- Middleware: RequireSession (cookie), RequireBearer (access token),
  RequireAuth (either), RequireAdmin (role check)
- Structured auth event logging (login, token issuance, session lifecycle)
- Schema migration 002_auth.sql extending users, oauth_clients, oauth_tokens
  tables and adding sessions, oauth_authorization_codes tables
- Initial admin user auto-created on first run with random password printed
  to stdout
- All tests pass with CGO_ENABLED=0, zero external runtime dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:59:37 +02:00

172 lines
4.5 KiB
Go

package auth
import (
"context"
"testing"
"time"
)
func TestSessionStore_CreateAndGet(t *testing.T) {
db := newTestDB(t)
userStore := NewSQLiteUserStore(db, 10)
sessStore := NewSQLiteSessionStore(db)
ctx := context.Background()
user, err := userStore.CreateUser(ctx, "sessuser", "password123", "")
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
session, err := sessStore.CreateSession(ctx, user.ID, 24*time.Hour)
if err != nil {
t.Fatalf("CreateSession: %v", err)
}
if session.SessionID == "" {
t.Error("SessionID should not be empty")
}
if session.UserID != user.ID {
t.Errorf("UserID = %d, want %d", session.UserID, user.ID)
}
// Get session
got, err := sessStore.GetSession(ctx, session.SessionID)
if err != nil {
t.Fatalf("GetSession: %v", err)
}
if got.UserID != user.ID {
t.Errorf("UserID = %d, want %d", got.UserID, user.ID)
}
}
func TestSessionStore_NotFound(t *testing.T) {
db := newTestDB(t)
sessStore := NewSQLiteSessionStore(db)
ctx := context.Background()
_, err := sessStore.GetSession(ctx, "nonexistent-session-id")
if err != ErrSessionNotFound {
t.Errorf("expected ErrSessionNotFound, got %v", err)
}
}
func TestSessionStore_Expired(t *testing.T) {
db := newTestDB(t)
userStore := NewSQLiteUserStore(db, 10)
sessStore := NewSQLiteSessionStore(db)
ctx := context.Background()
user, _ := userStore.CreateUser(ctx, "expuser", "password123", "")
// Create a session with very short lifetime
session, err := sessStore.CreateSession(ctx, user.ID, 1*time.Millisecond)
if err != nil {
t.Fatalf("CreateSession: %v", err)
}
// Wait for expiry
time.Sleep(10 * time.Millisecond)
_, err = sessStore.GetSession(ctx, session.SessionID)
if err != ErrSessionExpired {
t.Errorf("expected ErrSessionExpired, got %v", err)
}
}
func TestSessionStore_Delete(t *testing.T) {
db := newTestDB(t)
userStore := NewSQLiteUserStore(db, 10)
sessStore := NewSQLiteSessionStore(db)
ctx := context.Background()
user, _ := userStore.CreateUser(ctx, "deluser", "password123", "")
session, _ := sessStore.CreateSession(ctx, user.ID, 24*time.Hour)
if err := sessStore.DeleteSession(ctx, session.SessionID); err != nil {
t.Fatalf("DeleteSession: %v", err)
}
_, err := sessStore.GetSession(ctx, session.SessionID)
if err != ErrSessionNotFound {
t.Errorf("expected ErrSessionNotFound after delete, got %v", err)
}
}
func TestSessionStore_DeleteByUser(t *testing.T) {
db := newTestDB(t)
userStore := NewSQLiteUserStore(db, 10)
sessStore := NewSQLiteSessionStore(db)
ctx := context.Background()
user, _ := userStore.CreateUser(ctx, "delbyuser", "password123", "")
sess1, _ := sessStore.CreateSession(ctx, user.ID, 24*time.Hour)
sess2, _ := sessStore.CreateSession(ctx, user.ID, 24*time.Hour)
if err := sessStore.DeleteSessionsByUser(ctx, user.ID); err != nil {
t.Fatalf("DeleteSessionsByUser: %v", err)
}
_, err := sessStore.GetSession(ctx, sess1.SessionID)
if err != ErrSessionNotFound {
t.Errorf("session 1 should be deleted")
}
_, err = sessStore.GetSession(ctx, sess2.SessionID)
if err != ErrSessionNotFound {
t.Errorf("session 2 should be deleted")
}
}
func TestSessionStore_DeleteByUserExcept(t *testing.T) {
db := newTestDB(t)
userStore := NewSQLiteUserStore(db, 10)
sessStore := NewSQLiteSessionStore(db)
ctx := context.Background()
user, _ := userStore.CreateUser(ctx, "exceptuser", "password123", "")
sess1, _ := sessStore.CreateSession(ctx, user.ID, 24*time.Hour)
sess2, _ := sessStore.CreateSession(ctx, user.ID, 24*time.Hour)
if err := sessStore.DeleteSessionsByUserExcept(ctx, user.ID, sess1.SessionID); err != nil {
t.Fatalf("DeleteSessionsByUserExcept: %v", err)
}
// Session 1 should still exist
_, err := sessStore.GetSession(ctx, sess1.SessionID)
if err != nil {
t.Errorf("session 1 should still exist: %v", err)
}
// Session 2 should be deleted
_, err = sessStore.GetSession(ctx, sess2.SessionID)
if err != ErrSessionNotFound {
t.Errorf("session 2 should be deleted")
}
}
func TestSessionStore_CleanupExpired(t *testing.T) {
db := newTestDB(t)
userStore := NewSQLiteUserStore(db, 10)
sessStore := NewSQLiteSessionStore(db)
ctx := context.Background()
user, _ := userStore.CreateUser(ctx, "cleanuser", "password123", "")
// Create an expired session
sessStore.CreateSession(ctx, user.ID, 1*time.Millisecond)
time.Sleep(10 * time.Millisecond)
// Create a valid session
sessStore.CreateSession(ctx, user.ID, 24*time.Hour)
removed, err := sessStore.CleanupExpired(ctx)
if err != nil {
t.Fatalf("CleanupExpired: %v", err)
}
if removed != 1 {
t.Errorf("removed = %d, want 1", removed)
}
}