"""Tests for UserService. Tests the user service CRUD operations and OAuth-based user creation. Uses real Postgres via the db_session fixture from conftest. """ from datetime import UTC, datetime, timedelta import pytest from app.db.models import User from app.schemas.user import OAuthUserInfo, UserCreate, UserUpdate from app.services.user_service import user_service # Import db_session fixture from db conftest pytestmark = pytest.mark.asyncio class TestGetById: """Tests for get_by_id method.""" async def test_returns_user_when_found(self, db_session): """Test that get_by_id returns user when it exists. Creates a user and verifies it can be retrieved by ID. """ # Create user directly user = User( email="test@example.com", display_name="Test User", oauth_provider="google", oauth_id="123456", ) db_session.add(user) await db_session.commit() # Retrieve by ID from uuid import UUID user_id = UUID(user.id) if isinstance(user.id, str) else user.id result = await user_service.get_by_id(db_session, user_id) assert result is not None assert result.email == "test@example.com" async def test_returns_none_when_not_found(self, db_session): """Test that get_by_id returns None for nonexistent users.""" from uuid import uuid4 result = await user_service.get_by_id(db_session, uuid4()) assert result is None class TestGetByEmail: """Tests for get_by_email method.""" async def test_returns_user_when_found(self, db_session): """Test that get_by_email returns user when it exists.""" user = User( email="findme@example.com", display_name="Find Me", oauth_provider="discord", oauth_id="discord123", ) db_session.add(user) await db_session.commit() result = await user_service.get_by_email(db_session, "findme@example.com") assert result is not None assert result.display_name == "Find Me" async def test_returns_none_when_not_found(self, db_session): """Test that get_by_email returns None for nonexistent emails.""" result = await user_service.get_by_email(db_session, "nobody@example.com") assert result is None class TestGetByOAuth: """Tests for get_by_oauth method.""" async def test_returns_user_when_found(self, db_session): """Test that get_by_oauth returns user for matching provider+id.""" user = User( email="oauth@example.com", display_name="OAuth User", oauth_provider="google", oauth_id="google-unique-id", ) db_session.add(user) await db_session.commit() result = await user_service.get_by_oauth(db_session, "google", "google-unique-id") assert result is not None assert result.email == "oauth@example.com" async def test_returns_none_for_wrong_provider(self, db_session): """Test that get_by_oauth returns None if provider doesn't match.""" user = User( email="oauth2@example.com", display_name="OAuth User 2", oauth_provider="google", oauth_id="google-id-2", ) db_session.add(user) await db_session.commit() # Same ID, different provider result = await user_service.get_by_oauth(db_session, "discord", "google-id-2") assert result is None async def test_returns_none_when_not_found(self, db_session): """Test that get_by_oauth returns None for nonexistent OAuth.""" result = await user_service.get_by_oauth(db_session, "google", "nonexistent") assert result is None class TestCreate: """Tests for create method.""" async def test_creates_user_with_all_fields(self, db_session): """Test that create properly persists all user fields.""" user_data = UserCreate( email="new@example.com", display_name="New User", avatar_url="https://example.com/avatar.jpg", oauth_provider="discord", oauth_id="discord-new-id", ) result = await user_service.create(db_session, user_data) assert result.id is not None assert result.email == "new@example.com" assert result.display_name == "New User" assert result.avatar_url == "https://example.com/avatar.jpg" assert result.oauth_provider == "discord" assert result.oauth_id == "discord-new-id" assert result.is_premium is False assert result.premium_until is None async def test_creates_user_without_avatar(self, db_session): """Test that create works without optional avatar_url.""" user_data = UserCreate( email="noavatar@example.com", display_name="No Avatar", oauth_provider="google", oauth_id="google-no-avatar", ) result = await user_service.create(db_session, user_data) assert result.avatar_url is None class TestCreateFromOAuth: """Tests for create_from_oauth method.""" async def test_creates_user_from_oauth_info(self, db_session): """Test that create_from_oauth converts OAuthUserInfo to User.""" oauth_info = OAuthUserInfo( provider="google", oauth_id="google-oauth-123", email="oauthcreate@example.com", name="OAuth Created User", avatar_url="https://google.com/avatar.jpg", ) result = await user_service.create_from_oauth(db_session, oauth_info) assert result.email == "oauthcreate@example.com" assert result.display_name == "OAuth Created User" assert result.oauth_provider == "google" assert result.oauth_id == "google-oauth-123" class TestGetOrCreateFromOAuth: """Tests for get_or_create_from_oauth method.""" async def test_returns_existing_user_by_oauth(self, db_session): """Test that existing user is returned when OAuth matches. Verifies the method returns (user, False) for existing users. """ # Create existing user existing = User( email="existing@example.com", display_name="Existing", oauth_provider="google", oauth_id="existing-oauth-id", ) db_session.add(existing) await db_session.commit() # Try to get or create with same OAuth oauth_info = OAuthUserInfo( provider="google", oauth_id="existing-oauth-id", email="existing@example.com", name="Existing", ) result, created = await user_service.get_or_create_from_oauth(db_session, oauth_info) assert created is False assert result.id == existing.id async def test_links_existing_user_by_email(self, db_session): """Test that OAuth is linked when email matches existing user. If a user exists with the same email but different OAuth, the new OAuth should be linked to the existing account. """ # Create user with Google existing = User( email="link@example.com", display_name="Link Me", oauth_provider="google", oauth_id="google-link-id", ) db_session.add(existing) await db_session.commit() # Login with Discord (same email) oauth_info = OAuthUserInfo( provider="discord", oauth_id="discord-link-id", email="link@example.com", name="Link Me", avatar_url="https://discord.com/avatar.jpg", ) result, created = await user_service.get_or_create_from_oauth(db_session, oauth_info) assert created is False assert result.id == existing.id # OAuth should be updated to Discord assert result.oauth_provider == "discord" assert result.oauth_id == "discord-link-id" async def test_creates_new_user_when_not_found(self, db_session): """Test that new user is created when no match exists. Verifies the method returns (user, True) for new users. """ oauth_info = OAuthUserInfo( provider="discord", oauth_id="brand-new-id", email="brandnew@example.com", name="Brand New", ) result, created = await user_service.get_or_create_from_oauth(db_session, oauth_info) assert created is True assert result.email == "brandnew@example.com" class TestUpdate: """Tests for update method.""" async def test_updates_display_name(self, db_session): """Test that update changes display_name when provided.""" user = User( email="update@example.com", display_name="Old Name", oauth_provider="google", oauth_id="update-id", ) db_session.add(user) await db_session.commit() update_data = UserUpdate(display_name="New Name") result = await user_service.update(db_session, user, update_data) assert result.display_name == "New Name" async def test_updates_avatar_url(self, db_session): """Test that update changes avatar_url when provided.""" user = User( email="avatar@example.com", display_name="Avatar User", oauth_provider="google", oauth_id="avatar-id", ) db_session.add(user) await db_session.commit() update_data = UserUpdate(avatar_url="https://new-avatar.com/img.jpg") result = await user_service.update(db_session, user, update_data) assert result.avatar_url == "https://new-avatar.com/img.jpg" async def test_ignores_none_values(self, db_session): """Test that update doesn't change fields set to None. Only explicitly provided fields should be updated. """ user = User( email="keep@example.com", display_name="Keep Me", avatar_url="https://keep.com/avatar.jpg", oauth_provider="google", oauth_id="keep-id", ) db_session.add(user) await db_session.commit() # Update only display_name, leave avatar alone update_data = UserUpdate(display_name="Changed") result = await user_service.update(db_session, user, update_data) assert result.display_name == "Changed" assert result.avatar_url == "https://keep.com/avatar.jpg" class TestUpdateLastLogin: """Tests for update_last_login method.""" async def test_updates_last_login_timestamp(self, db_session): """Test that update_last_login sets current timestamp.""" user = User( email="login@example.com", display_name="Login User", oauth_provider="google", oauth_id="login-id", ) db_session.add(user) await db_session.commit() assert user.last_login is None before = datetime.now(UTC) result = await user_service.update_last_login(db_session, user) after = datetime.now(UTC) assert result.last_login is not None # Allow 1 second tolerance assert before - timedelta(seconds=1) <= result.last_login <= after + timedelta(seconds=1) class TestUpdatePremium: """Tests for update_premium method.""" async def test_grants_premium(self, db_session): """Test that update_premium sets premium status and expiration.""" user = User( email="premium@example.com", display_name="Premium User", oauth_provider="google", oauth_id="premium-id", ) db_session.add(user) await db_session.commit() assert user.is_premium is False expires = datetime.now(UTC) + timedelta(days=30) result = await user_service.update_premium(db_session, user, expires) assert result.is_premium is True assert result.premium_until == expires async def test_removes_premium(self, db_session): """Test that update_premium with None removes premium status.""" user = User( email="unpremium@example.com", display_name="Unpremium User", oauth_provider="google", oauth_id="unpremium-id", is_premium=True, premium_until=datetime.now(UTC) + timedelta(days=30), ) db_session.add(user) await db_session.commit() result = await user_service.update_premium(db_session, user, None) assert result.is_premium is False assert result.premium_until is None class TestDelete: """Tests for delete method.""" async def test_deletes_user(self, db_session): """Test that delete removes user from database.""" user = User( email="delete@example.com", display_name="Delete Me", oauth_provider="google", oauth_id="delete-id", ) db_session.add(user) await db_session.commit() user_id = user.id await user_service.delete(db_session, user) # Verify user is gone from uuid import UUID result = await user_service.get_by_id( db_session, UUID(user_id) if isinstance(user_id, str) else user_id ) assert result is None