feat(backend): saas users app settings api (#13021)

This commit is contained in:
Hiep Le
2026-02-27 13:01:03 +07:00
committed by GitHub
parent 3804b66e32
commit ddd544f8d6
8 changed files with 951 additions and 0 deletions

View File

@@ -47,6 +47,7 @@ from server.routes.org_invitations import ( # noqa: E402
from server.routes.orgs import org_router # noqa: E402
from server.routes.readiness import readiness_router # noqa: E402
from server.routes.user import saas_user_router # noqa: E402
from server.routes.user_app_settings import user_app_settings_router # noqa: E402
from server.routes.verified_models import ( # noqa: E402
api_router as verified_models_router,
)
@@ -79,6 +80,7 @@ base_app.include_router(api_router) # Add additional route for github auth
base_app.include_router(oauth_router) # Add additional route for oauth callback
base_app.include_router(oauth_device_router) # Add OAuth 2.0 Device Flow routes
base_app.include_router(saas_user_router) # Add additional route SAAS user calls
base_app.include_router(user_app_settings_router) # Add routes for user app settings
base_app.include_router(
billing_router
) # Add routes for credit management and Stripe payment integration

View File

@@ -0,0 +1,115 @@
"""Routes for user app settings API.
Provides endpoints for managing user-level app preferences:
- GET /api/users/app - Retrieve current user's app settings
- POST /api/users/app - Update current user's app settings
"""
from fastapi import APIRouter, Depends, HTTPException, status
from server.routes.user_app_settings_models import (
UserAppSettingsResponse,
UserAppSettingsUpdate,
UserNotFoundError,
)
from server.services.user_app_settings_service import (
UserAppSettingsService,
UserAppSettingsServiceInjector,
)
from openhands.core.logger import openhands_logger as logger
user_app_settings_router = APIRouter(prefix='/api/users')
# Create injector instance and dependency at module level
_injector = UserAppSettingsServiceInjector()
user_app_settings_service_dependency = Depends(_injector.depends)
@user_app_settings_router.get('/app', response_model=UserAppSettingsResponse)
async def get_user_app_settings(
service: UserAppSettingsService = user_app_settings_service_dependency,
) -> UserAppSettingsResponse:
"""Get the current user's app settings.
Returns language, analytics consent, sound notifications, and git config.
Args:
service: UserAppSettingsService (injected by dependency)
Returns:
UserAppSettingsResponse: The user's app settings
Raises:
HTTPException: 401 if user is not authenticated
HTTPException: 404 if user not found
HTTPException: 500 if retrieval fails
"""
try:
return await service.get_user_app_settings()
except ValueError as e:
# User not authenticated
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e),
)
except UserNotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
except Exception as e:
logger.exception(
'Unexpected error retrieving user app settings',
extra={'error': str(e)},
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to retrieve user app settings',
)
@user_app_settings_router.post('/app', response_model=UserAppSettingsResponse)
async def update_user_app_settings(
update_data: UserAppSettingsUpdate,
service: UserAppSettingsService = user_app_settings_service_dependency,
) -> UserAppSettingsResponse:
"""Update the current user's app settings (partial update).
Only provided fields will be updated. Pass null to clear a field.
Args:
update_data: Fields to update
service: UserAppSettingsService (injected by dependency)
Returns:
UserAppSettingsResponse: The updated user's app settings
Raises:
HTTPException: 401 if user is not authenticated
HTTPException: 404 if user not found
HTTPException: 500 if update fails
"""
try:
return await service.update_user_app_settings(update_data)
except ValueError as e:
# User not authenticated
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e),
)
except UserNotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
except Exception as e:
logger.exception(
'Failed to update user app settings',
extra={'error': str(e)},
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to update user app settings',
)

View File

@@ -0,0 +1,57 @@
"""
Pydantic models for user app settings API.
"""
from pydantic import BaseModel, EmailStr
from storage.user import User
class UserAppSettingsError(Exception):
"""Base exception for user app settings errors."""
pass
class UserNotFoundError(UserAppSettingsError):
"""Raised when user is not found."""
def __init__(self, user_id: str):
self.user_id = user_id
super().__init__(f'User with id "{user_id}" not found')
class UserAppSettingsUpdateError(UserAppSettingsError):
"""Raised when user app settings update fails."""
pass
class UserAppSettingsResponse(BaseModel):
"""Response model for user app settings."""
language: str | None = None
user_consents_to_analytics: bool | None = None
enable_sound_notifications: bool | None = None
git_user_name: str | None = None
git_user_email: EmailStr | None = None
@classmethod
def from_user(cls, user: User) -> 'UserAppSettingsResponse':
"""Create response from User entity."""
return cls(
language=user.language,
user_consents_to_analytics=user.user_consents_to_analytics,
enable_sound_notifications=user.enable_sound_notifications,
git_user_name=user.git_user_name,
git_user_email=user.git_user_email,
)
class UserAppSettingsUpdate(BaseModel):
"""Request model for updating user app settings (partial update)."""
language: str | None = None
user_consents_to_analytics: bool | None = None
enable_sound_notifications: bool | None = None
git_user_name: str | None = None
git_user_email: EmailStr | None = None

View File

@@ -0,0 +1,126 @@
"""Service class for managing user app settings.
Separates business logic from route handlers.
Uses dependency injection for db_session and user_context.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import AsyncGenerator
from fastapi import Request
from server.routes.user_app_settings_models import (
UserAppSettingsResponse,
UserAppSettingsUpdate,
UserNotFoundError,
)
from storage.user_app_settings_store import UserAppSettingsStore
from openhands.app_server.services.injector import Injector, InjectorState
from openhands.app_server.user.user_context import UserContext
from openhands.core.logger import openhands_logger as logger
@dataclass
class UserAppSettingsService:
"""Service for user app settings with injected dependencies."""
store: UserAppSettingsStore
user_context: UserContext
async def get_user_app_settings(self) -> UserAppSettingsResponse:
"""Get user app settings.
User ID is obtained from the injected user_context.
Returns:
UserAppSettingsResponse: The user's app settings
Raises:
ValueError: If user is not authenticated
UserNotFoundError: If user is not found
"""
user_id = await self.user_context.get_user_id()
if not user_id:
raise ValueError('User is not authenticated')
logger.info(
'Getting user app settings',
extra={'user_id': user_id},
)
user = await self.store.get_user_by_id(user_id)
if not user:
raise UserNotFoundError(user_id)
return UserAppSettingsResponse.from_user(user)
async def update_user_app_settings(
self,
update_data: UserAppSettingsUpdate,
) -> UserAppSettingsResponse:
"""Update user app settings.
Only updates fields that are explicitly provided in update_data.
User ID is obtained from the injected user_context.
Session auto-commits at request end via DbSessionInjector.
Args:
update_data: The update data from the request
Returns:
UserAppSettingsResponse: The updated user's app settings
Raises:
ValueError: If user is not authenticated
UserNotFoundError: If user is not found
"""
user_id = await self.user_context.get_user_id()
if not user_id:
raise ValueError('User is not authenticated')
logger.info(
'Updating user app settings',
extra={'user_id': user_id},
)
# Check if any fields are provided
update_dict = update_data.model_dump(exclude_unset=True)
if not update_dict:
# No fields to update, just return current settings
return await self.get_user_app_settings()
user = await self.store.update_user_app_settings(
user_id=user_id,
update_data=update_data,
)
if not user:
raise UserNotFoundError(user_id)
logger.info(
'User app settings updated successfully',
extra={'user_id': user_id, 'updated_fields': list(update_dict.keys())},
)
return UserAppSettingsResponse.from_user(user)
class UserAppSettingsServiceInjector(Injector[UserAppSettingsService]):
"""Injector that composes store and user_context for UserAppSettingsService."""
async def inject(
self, state: InjectorState, request: Request | None = None
) -> AsyncGenerator[UserAppSettingsService, None]:
# Local imports to avoid circular dependencies
from openhands.app_server.config import get_db_session, get_user_context
async with (
get_user_context(state, request) as user_context,
get_db_session(state, request) as db_session,
):
store = UserAppSettingsStore(db_session=db_session)
yield UserAppSettingsService(store=store, user_context=user_context)

View File

@@ -0,0 +1,64 @@
"""Store class for managing user app settings."""
from __future__ import annotations
import uuid
from dataclasses import dataclass
from server.routes.user_app_settings_models import UserAppSettingsUpdate
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from storage.user import User
@dataclass
class UserAppSettingsStore:
"""Store for user app settings with injected db_session."""
db_session: AsyncSession
async def get_user_by_id(self, user_id: str) -> User | None:
"""Get user by ID.
Args:
user_id: The user's ID (Keycloak user ID)
Returns:
User: The user object, or None if not found
"""
result = await self.db_session.execute(
select(User).filter(User.id == uuid.UUID(user_id))
)
return result.scalars().first()
async def update_user_app_settings(
self, user_id: str, update_data: UserAppSettingsUpdate
) -> User | None:
"""Update user app settings.
Only updates fields that are explicitly provided in update_data.
Uses flush() - commit happens at request end via DbSessionInjector.
Args:
user_id: The user's ID (Keycloak user ID)
update_data: Pydantic model with fields to update
Returns:
User: The updated user object, or None if user not found
"""
result = await self.db_session.execute(
select(User).filter(User.id == uuid.UUID(user_id)).with_for_update()
)
user = result.scalars().first()
if not user:
return None
# Update only explicitly provided fields
for field, value in update_data.model_dump(exclude_unset=True).items():
setattr(user, field, value)
# flush instead of commit - DbSessionInjector auto-commits at request end
await self.db_session.flush()
await self.db_session.refresh(user)
return user

View File

@@ -0,0 +1,207 @@
"""
Unit tests for user app settings API routes.
Tests the GET and POST /api/users/app endpoints.
"""
import uuid
from unittest.mock import AsyncMock, patch
import pytest
from fastapi import FastAPI, status
from fastapi.testclient import TestClient
from server.routes.user_app_settings import user_app_settings_router
from server.routes.user_app_settings_models import (
UserAppSettingsResponse,
UserNotFoundError,
)
from openhands.server.user_auth import get_user_id
TEST_USER_ID = str(uuid.uuid4())
@pytest.fixture
def mock_app():
"""Create a test FastAPI app with user app settings routes and mocked auth."""
app = FastAPI()
app.include_router(user_app_settings_router)
def mock_get_user_id():
return TEST_USER_ID
app.dependency_overrides[get_user_id] = mock_get_user_id
return app
@pytest.fixture
def mock_app_unauthenticated():
"""Create a test FastAPI app with no authenticated user."""
app = FastAPI()
app.include_router(user_app_settings_router)
def mock_get_user_id():
return None
app.dependency_overrides[get_user_id] = mock_get_user_id
return app
@pytest.fixture
def mock_settings_response():
"""Create a mock user app settings response."""
return UserAppSettingsResponse(
language='en',
user_consents_to_analytics=True,
enable_sound_notifications=False,
git_user_name='testuser',
git_user_email='test@example.com',
)
@pytest.mark.asyncio
async def test_get_user_app_settings_success(mock_app, mock_settings_response):
"""
GIVEN: An authenticated user with app settings
WHEN: GET /api/users/app is called
THEN: User's app settings are returned with 200 status
"""
# Arrange
with patch(
'server.routes.user_app_settings.UserAppSettingsService.get_user_app_settings',
AsyncMock(return_value=mock_settings_response),
):
client = TestClient(mock_app)
# Act
response = client.get('/api/users/app')
# Assert
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['language'] == 'en'
assert data['user_consents_to_analytics'] is True
assert data['enable_sound_notifications'] is False
assert data['git_user_name'] == 'testuser'
assert data['git_user_email'] == 'test@example.com'
@pytest.mark.asyncio
async def test_get_user_app_settings_not_authenticated(mock_app_unauthenticated):
"""
GIVEN: An unauthenticated request
WHEN: GET /api/users/app is called
THEN: 401 Unauthorized is returned
"""
# Arrange
client = TestClient(mock_app_unauthenticated)
# Act
response = client.get('/api/users/app')
# Assert
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert 'not authenticated' in response.json()['detail'].lower()
@pytest.mark.asyncio
async def test_get_user_app_settings_user_not_found(mock_app):
"""
GIVEN: An authenticated user that doesn't exist in the database
WHEN: GET /api/users/app is called
THEN: 404 Not Found is returned
"""
# Arrange
with patch(
'server.routes.user_app_settings.UserAppSettingsService.get_user_app_settings',
AsyncMock(side_effect=UserNotFoundError(TEST_USER_ID)),
):
client = TestClient(mock_app)
# Act
response = client.get('/api/users/app')
# Assert
assert response.status_code == status.HTTP_404_NOT_FOUND
assert 'not found' in response.json()['detail'].lower()
@pytest.mark.asyncio
async def test_update_user_app_settings_success(mock_app):
"""
GIVEN: An authenticated user
WHEN: POST /api/users/app is called with update data
THEN: Updated settings are returned with 200 status
"""
# Arrange
updated_response = UserAppSettingsResponse(
language='es',
user_consents_to_analytics=False,
enable_sound_notifications=True,
git_user_name='newuser',
git_user_email='new@example.com',
)
request_data = {
'language': 'es',
'user_consents_to_analytics': False,
}
with patch(
'server.routes.user_app_settings.UserAppSettingsService.update_user_app_settings',
AsyncMock(return_value=updated_response),
):
client = TestClient(mock_app)
# Act
response = client.post('/api/users/app', json=request_data)
# Assert
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['language'] == 'es'
assert data['user_consents_to_analytics'] is False
@pytest.mark.asyncio
async def test_update_user_app_settings_not_authenticated(mock_app_unauthenticated):
"""
GIVEN: An unauthenticated request
WHEN: POST /api/users/app is called
THEN: 401 Unauthorized is returned
"""
# Arrange
request_data = {'language': 'en'}
client = TestClient(mock_app_unauthenticated)
# Act
response = client.post('/api/users/app', json=request_data)
# Assert
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert 'not authenticated' in response.json()['detail'].lower()
@pytest.mark.asyncio
async def test_update_user_app_settings_user_not_found(mock_app):
"""
GIVEN: An authenticated user that doesn't exist in the database
WHEN: POST /api/users/app is called
THEN: 404 Not Found is returned
"""
# Arrange
request_data = {'language': 'en'}
with patch(
'server.routes.user_app_settings.UserAppSettingsService.update_user_app_settings',
AsyncMock(side_effect=UserNotFoundError(TEST_USER_ID)),
):
client = TestClient(mock_app)
# Act
response = client.post('/api/users/app', json=request_data)
# Assert
assert response.status_code == status.HTTP_404_NOT_FOUND
assert 'not found' in response.json()['detail'].lower()

View File

@@ -0,0 +1,176 @@
"""
Unit tests for UserAppSettingsService.
Tests the service layer for user app settings operations.
"""
import uuid
from unittest.mock import AsyncMock, MagicMock
import pytest
from server.routes.user_app_settings_models import (
UserAppSettingsResponse,
UserAppSettingsUpdate,
UserNotFoundError,
)
from server.services.user_app_settings_service import UserAppSettingsService
from storage.user import User
@pytest.fixture
def user_id():
"""Create a test user ID."""
return str(uuid.uuid4())
@pytest.fixture
def mock_user(user_id):
"""Create a mock user with app settings."""
user = MagicMock(spec=User)
user.id = uuid.UUID(user_id)
user.language = 'en'
user.user_consents_to_analytics = True
user.enable_sound_notifications = False
user.git_user_name = 'testuser'
user.git_user_email = 'test@example.com'
return user
@pytest.fixture
def mock_store():
"""Create a mock UserAppSettingsStore."""
return MagicMock()
@pytest.fixture
def mock_user_context(user_id):
"""Create a mock UserContext that returns the user_id."""
context = MagicMock()
context.get_user_id = AsyncMock(return_value=user_id)
return context
@pytest.mark.asyncio
async def test_get_user_app_settings_success(
user_id, mock_user, mock_store, mock_user_context
):
"""
GIVEN: A user exists in the database
WHEN: get_user_app_settings is called
THEN: UserAppSettingsResponse is returned with correct data
"""
# Arrange
mock_store.get_user_by_id = AsyncMock(return_value=mock_user)
service = UserAppSettingsService(store=mock_store, user_context=mock_user_context)
# Act
result = await service.get_user_app_settings()
# Assert
assert isinstance(result, UserAppSettingsResponse)
assert result.language == 'en'
assert result.user_consents_to_analytics is True
assert result.enable_sound_notifications is False
assert result.git_user_name == 'testuser'
assert result.git_user_email == 'test@example.com'
mock_store.get_user_by_id.assert_called_once_with(user_id)
@pytest.mark.asyncio
async def test_get_user_app_settings_user_not_found(
user_id, mock_store, mock_user_context
):
"""
GIVEN: A user does not exist in the database
WHEN: get_user_app_settings is called
THEN: UserNotFoundError is raised
"""
# Arrange
mock_store.get_user_by_id = AsyncMock(return_value=None)
service = UserAppSettingsService(store=mock_store, user_context=mock_user_context)
# Act & Assert
with pytest.raises(UserNotFoundError) as exc_info:
await service.get_user_app_settings()
assert user_id in str(exc_info.value)
@pytest.mark.asyncio
async def test_update_user_app_settings_success(
user_id, mock_user, mock_store, mock_user_context
):
"""
GIVEN: A user exists in the database
WHEN: update_user_app_settings is called with new values
THEN: UserAppSettingsResponse is returned with updated data
"""
# Arrange
mock_user.language = 'es'
mock_user.user_consents_to_analytics = False
update_data = UserAppSettingsUpdate(
language='es',
user_consents_to_analytics=False,
)
mock_store.update_user_app_settings = AsyncMock(return_value=mock_user)
service = UserAppSettingsService(store=mock_store, user_context=mock_user_context)
# Act
result = await service.update_user_app_settings(update_data)
# Assert
assert isinstance(result, UserAppSettingsResponse)
assert result.language == 'es'
assert result.user_consents_to_analytics is False
mock_store.update_user_app_settings.assert_called_once_with(
user_id=user_id, update_data=update_data
)
@pytest.mark.asyncio
async def test_update_user_app_settings_no_changes(
user_id, mock_user, mock_store, mock_user_context
):
"""
GIVEN: A user exists in the database
WHEN: update_user_app_settings is called with no fields
THEN: Current settings are returned without calling update
"""
# Arrange
update_data = UserAppSettingsUpdate() # No fields set
mock_store.get_user_by_id = AsyncMock(return_value=mock_user)
mock_store.update_user_app_settings = AsyncMock()
service = UserAppSettingsService(store=mock_store, user_context=mock_user_context)
# Act
result = await service.update_user_app_settings(update_data)
# Assert
assert isinstance(result, UserAppSettingsResponse)
mock_store.get_user_by_id.assert_called_once_with(user_id)
mock_store.update_user_app_settings.assert_not_called()
@pytest.mark.asyncio
async def test_update_user_app_settings_user_not_found(
user_id, mock_store, mock_user_context
):
"""
GIVEN: A user does not exist in the database
WHEN: update_user_app_settings is called
THEN: UserNotFoundError is raised
"""
# Arrange
update_data = UserAppSettingsUpdate(language='en')
mock_store.update_user_app_settings = AsyncMock(return_value=None)
service = UserAppSettingsService(store=mock_store, user_context=mock_user_context)
# Act & Assert
with pytest.raises(UserNotFoundError) as exc_info:
await service.update_user_app_settings(update_data)
assert user_id in str(exc_info.value)

View File

@@ -0,0 +1,204 @@
"""
Unit tests for UserAppSettingsStore.
Tests the async database operations for user app settings.
"""
import uuid
from unittest.mock import patch
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
# Mock the database module before importing
with patch('storage.database.engine', create=True), patch(
'storage.database.a_engine', create=True
):
from server.routes.user_app_settings_models import UserAppSettingsUpdate
from storage.base import Base
from storage.org import Org
from storage.user import User
from storage.user_app_settings_store import UserAppSettingsStore
@pytest.fixture
async def async_engine():
"""Create an async SQLite engine for testing."""
engine = create_async_engine(
'sqlite+aiosqlite:///:memory:',
poolclass=StaticPool,
connect_args={'check_same_thread': False},
echo=False,
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
await engine.dispose()
@pytest.fixture
async def async_session_maker(async_engine):
"""Create an async session maker for testing."""
return async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)
@pytest.mark.asyncio
async def test_get_user_by_id_success(async_session_maker):
"""
GIVEN: A user exists in the database
WHEN: get_user_by_id is called with the user's ID
THEN: The user is returned with correct data
"""
# Arrange
async with async_session_maker() as session:
org = Org(name='test-org')
session.add(org)
await session.flush()
user = User(
id=uuid.uuid4(),
current_org_id=org.id,
language='en',
user_consents_to_analytics=True,
enable_sound_notifications=False,
git_user_name='testuser',
git_user_email='test@example.com',
)
session.add(user)
await session.commit()
user_id = str(user.id)
# Act - create store with the session
store = UserAppSettingsStore(db_session=session)
result = await store.get_user_by_id(user_id)
# Assert
assert result is not None
assert str(result.id) == user_id
assert result.language == 'en'
assert result.user_consents_to_analytics is True
assert result.enable_sound_notifications is False
assert result.git_user_name == 'testuser'
assert result.git_user_email == 'test@example.com'
@pytest.mark.asyncio
async def test_get_user_by_id_not_found(async_session_maker):
"""
GIVEN: A user does not exist in the database
WHEN: get_user_by_id is called with a non-existent ID
THEN: None is returned
"""
# Arrange
non_existent_id = str(uuid.uuid4())
# Act
async with async_session_maker() as session:
store = UserAppSettingsStore(db_session=session)
result = await store.get_user_by_id(non_existent_id)
# Assert
assert result is None
@pytest.mark.asyncio
async def test_update_user_app_settings_success(async_session_maker):
"""
GIVEN: A user exists in the database
WHEN: update_user_app_settings is called with new values
THEN: The user's settings are updated and returned
"""
# Arrange
async with async_session_maker() as session:
org = Org(name='test-org')
session.add(org)
await session.flush()
user = User(
id=uuid.uuid4(),
current_org_id=org.id,
language='en',
user_consents_to_analytics=False,
)
session.add(user)
await session.commit()
user_id = str(user.id)
update_data = UserAppSettingsUpdate(
language='es',
user_consents_to_analytics=True,
enable_sound_notifications=True,
git_user_name='newuser',
git_user_email='new@example.com',
)
# Act - create store with the session
store = UserAppSettingsStore(db_session=session)
result = await store.update_user_app_settings(user_id, update_data)
# Assert
assert result is not None
assert result.language == 'es'
assert result.user_consents_to_analytics is True
assert result.enable_sound_notifications is True
assert result.git_user_name == 'newuser'
assert result.git_user_email == 'new@example.com'
@pytest.mark.asyncio
async def test_update_user_app_settings_partial(async_session_maker):
"""
GIVEN: A user exists with existing settings
WHEN: update_user_app_settings is called with only some fields
THEN: Only the provided fields are updated, others remain unchanged
"""
# Arrange
async with async_session_maker() as session:
org = Org(name='test-org')
session.add(org)
await session.flush()
user = User(
id=uuid.uuid4(),
current_org_id=org.id,
language='en',
user_consents_to_analytics=True,
git_user_name='original',
)
session.add(user)
await session.commit()
user_id = str(user.id)
# Only update language
update_data = UserAppSettingsUpdate(language='fr')
# Act - create store with the session
store = UserAppSettingsStore(db_session=session)
result = await store.update_user_app_settings(user_id, update_data)
# Assert
assert result is not None
assert result.language == 'fr'
assert result.user_consents_to_analytics is True # Unchanged
assert result.git_user_name == 'original' # Unchanged
@pytest.mark.asyncio
async def test_update_user_app_settings_user_not_found(async_session_maker):
"""
GIVEN: A user does not exist in the database
WHEN: update_user_app_settings is called
THEN: None is returned
"""
# Arrange
non_existent_id = str(uuid.uuid4())
update_data = UserAppSettingsUpdate(language='en')
# Act
async with async_session_maker() as session:
store = UserAppSettingsStore(db_session=session)
result = await store.update_user_app_settings(non_existent_id, update_data)
# Assert
assert result is None