package auth import ( "context" "database/sql" "testing" "time" "github.com/mjensen/weimar/internal/db" ) func setupTestDB(t *testing.T) *db.DB { t.Helper() database, err := db.Open(":memory:") if err != nil { t.Fatalf("open :memory: db: %v", err) } t.Cleanup(func() { database.Close() }) if err := database.Migrate(context.Background()); err != nil { t.Fatalf("migrate: %v", err) } return database } func TestRegister_Success(t *testing.T) { database := setupTestDB(t) ctx := context.Background() user, err := Register(ctx, database, "alice", "password123", false) if err != nil { t.Fatalf("Register: %v", err) } if user.Username != "alice" { t.Fatalf("username = %q, want alice", user.Username) } if user.IsAdmin { t.Fatal("expected non-admin user") } } func TestRegister_Admin(t *testing.T) { database := setupTestDB(t) ctx := context.Background() user, err := Register(ctx, database, "admin", "password123", true) if err != nil { t.Fatalf("Register: %v", err) } if !user.IsAdmin { t.Fatal("expected admin user") } } func TestRegister_UsernameTooShort(t *testing.T) { database := setupTestDB(t) _, err := Register(context.Background(), database, "ab", "password123", false) if err != ErrUsernameTooShort { t.Fatalf("expected ErrUsernameTooShort, got %v", err) } } func TestRegister_PasswordTooShort(t *testing.T) { database := setupTestDB(t) _, err := Register(context.Background(), database, "alice", "short", false) if err != ErrPasswordTooShort { t.Fatalf("expected ErrPasswordTooShort, got %v", err) } } func TestRegister_UsernameTaken(t *testing.T) { database := setupTestDB(t) ctx := context.Background() if _, err := Register(ctx, database, "alice", "password123", false); err != nil { t.Fatalf("first register: %v", err) } _, err := Register(ctx, database, "alice", "otherpass1", false) if err != ErrUsernameTaken { t.Fatalf("expected ErrUsernameTaken, got %v", err) } } func TestSessionManager_Login_Success(t *testing.T) { database := setupTestDB(t) ctx := context.Background() Register(ctx, database, "alice", "password123", false) sm := NewSessionManager(database) user, token, err := sm.Login(ctx, "alice", "password123") if err != nil { t.Fatalf("Login: %v", err) } if user.Username != "alice" { t.Fatalf("username = %q", user.Username) } if token == "" { t.Fatal("expected non-empty token") } if len(token) != 64 { t.Fatalf("token length = %d, want 64", len(token)) } } func TestSessionManager_Login_InvalidPassword(t *testing.T) { database := setupTestDB(t) ctx := context.Background() Register(ctx, database, "alice", "password123", false) sm := NewSessionManager(database) _, _, err := sm.Login(ctx, "alice", "wrongpass") if err != ErrInvalidCredentials { t.Fatalf("expected ErrInvalidCredentials, got %v", err) } } func TestSessionManager_Login_UserNotFound(t *testing.T) { database := setupTestDB(t) sm := NewSessionManager(database) _, _, err := sm.Login(context.Background(), "nobody", "password123") if err != ErrInvalidCredentials { t.Fatalf("expected ErrInvalidCredentials, got %v", err) } } func TestSessionManager_Authenticate_Success(t *testing.T) { database := setupTestDB(t) ctx := context.Background() Register(ctx, database, "alice", "password123", false) sm := NewSessionManager(database) _, token, _ := sm.Login(ctx, "alice", "password123") user, err := sm.Authenticate(ctx, token) if err != nil { t.Fatalf("Authenticate: %v", err) } if user.Username != "alice" { t.Fatalf("username = %q", user.Username) } } func TestSessionManager_Authenticate_Expired(t *testing.T) { database := setupTestDB(t) ctx := context.Background() // Create a user and a session directly with an already-expired timestamp. Register(ctx, database, "alice", "password123", false) user, _ := database.GetUserByUsername(ctx, "alice") token := "exptoken123456789012345678901234567890123456789012345678901234567890" expiredAt := time.Now().UTC().Add(-time.Hour) database.CreateSession(ctx, user.ID, token, expiredAt) sm := NewSessionManager(database) _, err := sm.Authenticate(ctx, token) if err != ErrSessionExpired { t.Fatalf("expected ErrSessionExpired, got %v", err) } // Session should be cleaned up. _, err = database.GetSessionByToken(ctx, token) if err != sql.ErrNoRows { t.Fatal("expired session should have been deleted") } } func TestSessionManager_Authenticate_TokenNotFound(t *testing.T) { database := setupTestDB(t) sm := NewSessionManager(database) _, err := sm.Authenticate(context.Background(), "nonexistenttoken") if err != ErrSessionNotFound { t.Fatalf("expected ErrSessionNotFound, got %v", err) } } func TestSessionManager_Logout(t *testing.T) { database := setupTestDB(t) ctx := context.Background() Register(ctx, database, "alice", "password123", false) sm := NewSessionManager(database) _, token, _ := sm.Login(ctx, "alice", "password123") if err := sm.Logout(ctx, token); err != nil { t.Fatalf("Logout: %v", err) } // Session should be gone. _, err := sm.Authenticate(ctx, token) if err != ErrSessionNotFound { t.Fatalf("expected ErrSessionNotFound after logout, got %v", err) } } func TestSessionManager_Authenticate_ExtendsExpiry(t *testing.T) { database := setupTestDB(t) ctx := context.Background() Register(ctx, database, "alice", "password123", false) sm := NewSessionManager(database) _, token, _ := sm.Login(ctx, "alice", "password123") // Authenticate should extend the session. if _, err := sm.Authenticate(ctx, token); err != nil { t.Fatalf("first Authenticate: %v", err) } sess, err := database.GetSessionByToken(ctx, token) if err != nil { t.Fatalf("GetSessionByToken: %v", err) } // Expiry should be far in the future (SessionDuration ~30 days). expectedMin := time.Now().UTC().Add(29 * 24 * time.Hour) if sess.ExpiresAt.Before(expectedMin) { t.Fatalf("session expires at %v, want after %v", sess.ExpiresAt, expectedMin) } }