From 540541eec6c1f39c469ac4d2d000b627b3f6581c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Feb 2026 09:33:48 +0000 Subject: [PATCH] style: apply ruff auto-fix - Auto-formatted code with ruff format - Applied ruff linting fixes with --fix Co-authored-by: github-actions[bot] --- app/api/dropbox.py | 62 +++----------- app/api/google_drive.py | 75 ++++------------ app/api/onedrive.py | 104 ++++++----------------- app/api/settings.py | 92 ++++++-------------- app/celery_worker.py | 23 +++-- app/models.py | 43 +++------- app/utils/settings_service.py | 85 +++++------------- app/views/settings.py | 21 ++--- app/views/wizard.py | 22 ++--- tests/test_api_dropbox.py | 8 +- tests/test_api_onedrive_comprehensive.py | 64 ++++---------- tests/test_settings_audit_log.py | 82 +++++------------- tests/test_wizard_db_persist.py | 62 +++----------- 13 files changed, 193 insertions(+), 550 deletions(-) diff --git a/app/api/dropbox.py b/app/api/dropbox.py index 57e32d09..f9bf10e5 100644 --- a/app/api/dropbox.py +++ b/app/api/dropbox.py @@ -49,9 +49,7 @@ async def exchange_dropbox_token( } # Use shared OAuth helper (handles secure logging and error handling) - token_data = exchange_oauth_token( - provider_name="Dropbox", token_url=token_url, payload=payload - ) + token_data = exchange_oauth_token(provider_name="Dropbox", token_url=token_url, payload=payload) # Return just what's needed by the frontend return { @@ -79,19 +77,13 @@ async def update_dropbox_settings( user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "wizard" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard" ) # Update settings in memory and persist to database if refresh_token: settings.dropbox_refresh_token = refresh_token - save_setting_to_db( - db, "dropbox_refresh_token", refresh_token, changed_by=changed_by - ) + save_setting_to_db(db, "dropbox_refresh_token", refresh_token, changed_by=changed_by) logger.info("Updated DROPBOX_REFRESH_TOKEN in memory and database") if app_key: @@ -101,9 +93,7 @@ async def update_dropbox_settings( if app_secret: settings.dropbox_app_secret = app_secret - save_setting_to_db( - db, "dropbox_app_secret", app_secret, changed_by=changed_by - ) + save_setting_to_db(db, "dropbox_app_secret", app_secret, changed_by=changed_by) logger.info("Updated DROPBOX_APP_SECRET in memory and database") if folder_path: @@ -135,11 +125,7 @@ async def test_dropbox_token(request: Request): try: logger.info("Testing Dropbox token validity") - if ( - not settings.dropbox_refresh_token - or not settings.dropbox_app_key - or not settings.dropbox_app_secret - ): + if not settings.dropbox_refresh_token or not settings.dropbox_app_key or not settings.dropbox_app_secret: logger.warning("Dropbox credentials not fully configured") return { "status": "error", @@ -167,14 +153,10 @@ async def test_dropbox_token(request: Request): "client_secret": settings.dropbox_app_secret, } - refresh_response = requests.post( - refresh_url, data=refresh_data, timeout=settings.http_request_timeout - ) + refresh_response = requests.post(refresh_url, data=refresh_data, timeout=settings.http_request_timeout) if refresh_response.status_code != 200: - logger.error( - f"Failed to refresh Dropbox token: {refresh_response.text}" - ) + logger.error(f"Failed to refresh Dropbox token: {refresh_response.text}") return { "status": "error", "message": "Refresh token has expired or is invalid", @@ -193,9 +175,7 @@ async def test_dropbox_token(request: Request): ) if response.status_code != 200: - logger.error( - f"Dropbox token test failed: {response.status_code} {response.text}" - ) + logger.error(f"Dropbox token test failed: {response.status_code} {response.text}") return { "status": "error", "message": f"Token validation failed with status {response.status_code}: {response.text}", @@ -243,11 +223,7 @@ async def save_dropbox_settings( try: user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "wizard" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard" ) # Update settings in memory @@ -262,27 +238,19 @@ async def save_dropbox_settings( # Persist to database (primary storage) if refresh_token: - save_setting_to_db( - db, "dropbox_refresh_token", refresh_token, changed_by=changed_by - ) + save_setting_to_db(db, "dropbox_refresh_token", refresh_token, changed_by=changed_by) if app_key: save_setting_to_db(db, "dropbox_app_key", app_key, changed_by=changed_by) if app_secret: - save_setting_to_db( - db, "dropbox_app_secret", app_secret, changed_by=changed_by - ) + save_setting_to_db(db, "dropbox_app_secret", app_secret, changed_by=changed_by) if folder_path: save_setting_to_db(db, "dropbox_folder", folder_path, changed_by=changed_by) # Best-effort .env file write try: - env_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env" - ) + env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env") if not os.path.exists(env_path): - logger.warning( - f".env file not found at {env_path}, skipping file write" - ) + logger.warning(f".env file not found at {env_path}, skipping file write") else: logger.info(f"Updating Dropbox settings in {env_path}") @@ -303,9 +271,7 @@ async def save_dropbox_settings( stripped_line = line.rstrip() is_updated = False for key, value in dropbox_settings.items(): - if stripped_line.startswith( - f"{key}=" - ) or stripped_line.startswith(f"# {key}="): + if stripped_line.startswith(f"{key}=") or stripped_line.startswith(f"# {key}="): new_env_lines.append(f"{key}={value}") updated.add(key) is_updated = True diff --git a/app/api/google_drive.py b/app/api/google_drive.py index 54a6028e..f9eda756 100644 --- a/app/api/google_drive.py +++ b/app/api/google_drive.py @@ -49,9 +49,7 @@ async def exchange_google_drive_token( } # Use shared OAuth helper (handles secure logging and error handling) - token_data = exchange_oauth_token( - provider_name="Google Drive", token_url=token_url, payload=payload - ) + token_data = exchange_oauth_token(provider_name="Google Drive", token_url=token_url, payload=payload) # Return just what's needed by the frontend return { @@ -80,11 +78,7 @@ async def update_google_drive_settings( user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "wizard" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard" ) # Convert use_oauth string to boolean @@ -93,30 +87,22 @@ async def update_google_drive_settings( # Update settings in memory and persist to database if refresh_token: settings.google_drive_refresh_token = refresh_token - save_setting_to_db( - db, "google_drive_refresh_token", refresh_token, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_refresh_token", refresh_token, changed_by=changed_by) logger.info("Updated GOOGLE_DRIVE_REFRESH_TOKEN in memory and database") if client_id: settings.google_drive_client_id = client_id - save_setting_to_db( - db, "google_drive_client_id", client_id, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_client_id", client_id, changed_by=changed_by) logger.info("Updated GOOGLE_DRIVE_CLIENT_ID in memory and database") if client_secret: settings.google_drive_client_secret = client_secret - save_setting_to_db( - db, "google_drive_client_secret", client_secret, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_client_secret", client_secret, changed_by=changed_by) logger.info("Updated GOOGLE_DRIVE_CLIENT_SECRET in memory and database") if folder_id: settings.google_drive_folder_id = folder_id - save_setting_to_db( - db, "google_drive_folder_id", folder_id, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_folder_id", folder_id, changed_by=changed_by) logger.info("Updated GOOGLE_DRIVE_FOLDER_ID in memory and database") # Set the OAuth flag @@ -127,9 +113,7 @@ async def update_google_drive_settings( str(use_oauth_bool).lower(), changed_by=changed_by, ) - logger.info( - f"Updated GOOGLE_DRIVE_USE_OAUTH in memory and database to {use_oauth_bool}" - ) + logger.info(f"Updated GOOGLE_DRIVE_USE_OAUTH in memory and database to {use_oauth_bool}") notify_settings_updated() @@ -154,8 +138,7 @@ async def test_google_drive_token(request: Request): Tests both OAuth and service account approaches based on configuration. """ try: - from app.tasks.upload_to_google_drive import (get_drive_service_oauth, - get_google_drive_service) + from app.tasks.upload_to_google_drive import get_drive_service_oauth, get_google_drive_service logger.info("Testing Google Drive token validity") @@ -232,9 +215,7 @@ async def test_google_drive_token(request: Request): else: # Test service account connection if not settings.google_drive_credentials_json: - logger.warning( - "Google Drive service account credentials not configured" - ) + logger.warning("Google Drive service account credentials not configured") return { "status": "error", "message": "Google Drive service account credentials are not configured", @@ -253,9 +234,7 @@ async def test_google_drive_token(request: Request): else: user_display = user_email - logger.info( - f"Successfully connected to Google Drive using service account as {user_display}" - ) + logger.info(f"Successfully connected to Google Drive using service account as {user_display}") return { "status": "success", @@ -398,20 +377,14 @@ async def save_dropbox_settings( """ try: # Get the path to the .env file - env_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env" - ) + env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env") # Convert use_oauth string to boolean use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t") user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "wizard" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard" ) # Define settings to update @@ -446,9 +419,7 @@ async def save_dropbox_settings( stripped_line = line.rstrip() is_updated = False for key, value in drive_settings.items(): - if stripped_line.startswith( - f"{key}=" - ) or stripped_line.startswith(f"# {key}="): + if stripped_line.startswith(f"{key}=") or stripped_line.startswith(f"# {key}="): # Uncomment if commented out - check the original stripped line new_env_lines.append(f"{key}={value}") updated.add(key) @@ -468,9 +439,7 @@ async def save_dropbox_settings( logger.info("Successfully updated Google Drive settings in .env file") except Exception as e: - logger.warning( - f"Failed to update .env file: {str(e)}, but will continue with in-memory update" - ) + logger.warning(f"Failed to update .env file: {str(e)}, but will continue with in-memory update") else: logger.warning( f".env file not found at {env_path}, skipping file update but continuing with in-memory update" @@ -497,21 +466,13 @@ async def save_dropbox_settings( changed_by=changed_by, ) if refresh_token: - save_setting_to_db( - db, "google_drive_refresh_token", refresh_token, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_refresh_token", refresh_token, changed_by=changed_by) if client_id: - save_setting_to_db( - db, "google_drive_client_id", client_id, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_client_id", client_id, changed_by=changed_by) if client_secret: - save_setting_to_db( - db, "google_drive_client_secret", client_secret, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_client_secret", client_secret, changed_by=changed_by) if folder_id: - save_setting_to_db( - db, "google_drive_folder_id", folder_id, changed_by=changed_by - ) + save_setting_to_db(db, "google_drive_folder_id", folder_id, changed_by=changed_by) notify_settings_updated() diff --git a/app/api/onedrive.py b/app/api/onedrive.py index 0329b406..e9f8328d 100644 --- a/app/api/onedrive.py +++ b/app/api/onedrive.py @@ -51,9 +51,7 @@ async def exchange_onedrive_token( } # Use shared OAuth helper (handles secure logging and error handling) - token_data = exchange_oauth_token( - provider_name="OneDrive", token_url=token_url, payload=payload - ) + token_data = exchange_oauth_token(provider_name="OneDrive", token_url=token_url, payload=payload) # Return just what's needed by the frontend return { @@ -94,9 +92,7 @@ async def test_onedrive_token(request: Request): "scope": "offline_access Files.ReadWrite", } - response = requests.post( - token_url, data=refresh_data, timeout=settings.http_request_timeout - ) + response = requests.post(token_url, data=refresh_data, timeout=settings.http_request_timeout) if response.status_code != 200: logger.error(f"Failed to refresh OneDrive token: {response.text}") @@ -108,25 +104,19 @@ async def test_onedrive_token(request: Request): token_data = response.json() access_token = token_data.get("access_token") - expires_in = token_data.get( - "expires_in", 3600 - ) # Default to 1 hour if not specified + expires_in = token_data.get("expires_in", 3600) # Default to 1 hour if not specified # Check if we got a new refresh token (Microsoft sometimes issues a new one) new_refresh_token = token_data.get("refresh_token") if new_refresh_token and new_refresh_token != settings.onedrive_refresh_token: - logger.info( - "Received new refresh token from Microsoft - will update configuration" - ) + logger.info("Received new refresh token from Microsoft - will update configuration") # Update refresh token in memory settings.onedrive_refresh_token = new_refresh_token # Also try to update .env file if it exists try: - env_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env" - ) + env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env") if os.path.exists(env_path): with open(env_path, "r") as f: env_lines = f.readlines() @@ -136,17 +126,13 @@ async def test_onedrive_token(request: Request): for line in env_lines: if line.startswith("ONEDRIVE_REFRESH_TOKEN="): - updated_lines.append( - f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n" - ) + updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n") updated = True else: updated_lines.append(line) if not updated: - updated_lines.append( - f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n" - ) + updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n") with open(env_path, "w") as f: f.writelines(updated_lines) @@ -172,22 +158,16 @@ async def test_onedrive_token(request: Request): finally: _db.close() except Exception as _e: - logger.warning( - f"Failed to persist rotated OneDrive refresh token to database: {_e}" - ) + logger.warning(f"Failed to persist rotated OneDrive refresh token to database: {_e}") # Test the access token by getting user information user_info_url = "https://graph.microsoft.com/v1.0/me" headers = {"Authorization": f"Bearer {access_token}"} - user_response = requests.get( - user_info_url, headers=headers, timeout=settings.http_request_timeout - ) + user_response = requests.get(user_info_url, headers=headers, timeout=settings.http_request_timeout) if user_response.status_code != 200: - logger.error( - f"OneDrive token test failed: {user_response.status_code} {user_response.text}" - ) + logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}") return { "status": "error", "message": f"Token validation failed with status {user_response.status_code}: {user_response.text}", @@ -263,22 +243,14 @@ async def save_onedrive_settings( try: user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "wizard" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard" ) # Best-effort .env file write try: - env_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env" - ) + env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env") if not os.path.exists(env_path): - logger.warning( - f".env file not found at {env_path}, skipping file write" - ) + logger.warning(f".env file not found at {env_path}, skipping file write") else: logger.info(f"Updating OneDrive settings in {env_path}") @@ -301,9 +273,7 @@ async def save_onedrive_settings( stripped_line = line.rstrip() is_updated = False for key, value in onedrive_settings.items(): - if stripped_line.startswith( - f"{key}=" - ) or stripped_line.startswith(f"# {key}="): + if stripped_line.startswith(f"{key}=") or stripped_line.startswith(f"# {key}="): new_env_lines.append(f"{key}={value}") updated.add(key) is_updated = True @@ -336,25 +306,15 @@ async def save_onedrive_settings( # Persist to database (primary) if refresh_token: - save_setting_to_db( - db, "onedrive_refresh_token", refresh_token, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_refresh_token", refresh_token, changed_by=changed_by) if client_id: - save_setting_to_db( - db, "onedrive_client_id", client_id, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_client_id", client_id, changed_by=changed_by) if client_secret: - save_setting_to_db( - db, "onedrive_client_secret", client_secret, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_client_secret", client_secret, changed_by=changed_by) if tenant_id: - save_setting_to_db( - db, "onedrive_tenant_id", tenant_id, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_tenant_id", tenant_id, changed_by=changed_by) if folder_path: - save_setting_to_db( - db, "onedrive_folder_path", folder_path, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_folder_path", folder_path, changed_by=changed_by) notify_settings_updated() @@ -388,47 +348,33 @@ async def update_onedrive_settings( user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "wizard" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard" ) # Update settings in memory and persist to database if refresh_token: settings.onedrive_refresh_token = refresh_token - save_setting_to_db( - db, "onedrive_refresh_token", refresh_token, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_refresh_token", refresh_token, changed_by=changed_by) logger.info("Updated ONEDRIVE_REFRESH_TOKEN in memory and database") if client_id: settings.onedrive_client_id = client_id - save_setting_to_db( - db, "onedrive_client_id", client_id, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_client_id", client_id, changed_by=changed_by) logger.info("Updated ONEDRIVE_CLIENT_ID in memory and database") if client_secret: settings.onedrive_client_secret = client_secret - save_setting_to_db( - db, "onedrive_client_secret", client_secret, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_client_secret", client_secret, changed_by=changed_by) logger.info("Updated ONEDRIVE_CLIENT_SECRET in memory and database") if tenant_id: settings.onedrive_tenant_id = tenant_id - save_setting_to_db( - db, "onedrive_tenant_id", tenant_id, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_tenant_id", tenant_id, changed_by=changed_by) logger.info("Updated ONEDRIVE_TENANT_ID in memory and database") if folder_path: settings.onedrive_folder_path = folder_path - save_setting_to_db( - db, "onedrive_folder_path", folder_path, changed_by=changed_by - ) + save_setting_to_db(db, "onedrive_folder_path", folder_path, changed_by=changed_by) logger.info("Updated ONEDRIVE_FOLDER_PATH in memory and database") notify_settings_updated() diff --git a/app/api/settings.py b/app/api/settings.py index a22ac742..75c9e99a 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -11,16 +11,19 @@ from sqlalchemy.orm import Session from app.config import settings from app.database import get_db -from app.utils.input_validation import (validate_setting_key, - validate_setting_key_format) -from app.utils.settings_service import (SETTING_METADATA, - delete_setting_from_db, - get_all_settings_from_db, - get_audit_log, get_setting_history, - get_setting_metadata, - get_settings_by_category, - rollback_setting, save_setting_to_db, - validate_setting_value) +from app.utils.input_validation import validate_setting_key, validate_setting_key_format +from app.utils.settings_service import ( + SETTING_METADATA, + delete_setting_from_db, + get_all_settings_from_db, + get_audit_log, + get_setting_history, + get_setting_metadata, + get_settings_by_category, + rollback_setting, + save_setting_to_db, + validate_setting_value, +) from app.utils.settings_sync import notify_settings_updated logger = logging.getLogger(__name__) @@ -37,9 +40,7 @@ def require_admin(request: Request) -> dict: """ user = request.session.get("user") if not user or not user.get("is_admin"): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required" - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return user @@ -93,9 +94,7 @@ async def get_settings(request: Request, db: DbSession, admin: AdminUser): # Get settings organized by category categories = get_settings_by_category() - return SettingsListResponse( - settings=current_settings, categories=categories, db_settings=db_settings - ) + return SettingsListResponse(settings=current_settings, categories=categories, db_settings=db_settings) except Exception as e: logger.error(f"Error retrieving settings: {e}") raise HTTPException( @@ -118,9 +117,7 @@ async def get_setting(key: str, request: Request, db: DbSession, admin: AdminUse # Get metadata metadata = get_setting_metadata(key) - return SettingResponse( - key=key, value=str(value) if value is not None else None, metadata=metadata - ) + return SettingResponse(key=key, value=str(value) if value is not None else None, metadata=metadata) except Exception as e: logger.error(f"Error retrieving setting {key}: {e}") raise HTTPException( @@ -147,18 +144,12 @@ async def update_setting( if setting.value is not None: is_valid, error_message = validate_setting_value(key, setting.value) if not is_valid: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=error_message - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_message) # Determine the username for the audit log user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "admin" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin" ) # Save to database @@ -203,11 +194,7 @@ async def delete_setting(key: str, request: Request, db: DbSession, admin: Admin try: user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "admin" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin" ) success = delete_setting_from_db(db, key, changed_by=changed_by) @@ -291,9 +278,7 @@ async def list_credentials(request: Request, db: DbSession, admin: AdminUser): @router.post("/bulk-update") -async def bulk_update_settings( - updates: list[SettingUpdate], request: Request, db: DbSession, admin: AdminUser -): +async def bulk_update_settings(updates: list[SettingUpdate], request: Request, db: DbSession, admin: AdminUser): """ Update multiple settings at once. Admin only. @@ -303,36 +288,24 @@ async def bulk_update_settings( user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "admin" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin" ) for update in updates: try: # Validate the setting value if update.value is not None: - is_valid, error_message = validate_setting_value( - update.key, update.value - ) + is_valid, error_message = validate_setting_value(update.key, update.value) if not is_valid: errors.append({"key": update.key, "error": error_message}) continue # Save to database - success = save_setting_to_db( - db, update.key, update.value, changed_by=changed_by - ) + success = save_setting_to_db(db, update.key, update.value, changed_by=changed_by) if success: - results.append( - {"key": update.key, "value": update.value, "status": "success"} - ) + results.append({"key": update.key, "value": update.value, "status": "success"}) else: - errors.append( - {"key": update.key, "error": "Failed to save to database"} - ) + errors.append({"key": update.key, "error": "Failed to save to database"}) except Exception as e: logger.error(f"Error updating setting {update.key}: {e}") errors.append({"key": update.key, "error": str(e)}) @@ -340,10 +313,7 @@ async def bulk_update_settings( if results: notify_settings_updated() - restart_required = any( - get_setting_metadata(result["key"]).get("restart_required", False) - for result in results - ) + restart_required = any(get_setting_metadata(result["key"]).get("restart_required", False) for result in results) return { "success": len(errors) == 0, @@ -422,11 +392,7 @@ async def rollback_setting_to_history( try: user = request.session.get("user", {}) if hasattr(request, "session") else {} changed_by = ( - user.get("preferred_username") - or user.get("username") - or user.get("email") - or user.get("id") - or "admin" + user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin" ) success = rollback_setting(db, key, history_id, changed_by=changed_by) @@ -498,9 +464,7 @@ async def export_env_settings( return FastAPIResponse( content=content, media_type="text/plain", - headers={ - "Content-Disposition": f'attachment; filename="docuelevate-{source}.env"' - }, + headers={"Content-Disposition": f'attachment; filename="docuelevate-{source}.env"'}, ) except Exception as e: logger.error(f"Error exporting settings: {e}") diff --git a/app/celery_worker.py b/app/celery_worker.py index ddc74225..d6bd83d4 100644 --- a/app/celery_worker.py +++ b/app/celery_worker.py @@ -3,32 +3,30 @@ from celery.schedules import crontab # Ensure tasks are loaded -from app import \ - tasks # noqa: F401 - Imports app/tasks.py so Celery can register tasks +from app import tasks # noqa: F401 - Imports app/tasks.py so Celery can register tasks + # Import the shared Celery instance from app.celery_app import celery from app.config import settings from app.tasks.check_credentials import check_credentials from app.tasks.convert_to_pdf import convert_to_pdf # noqa: F401 -from app.tasks.embed_metadata_into_pdf import \ - embed_metadata_into_pdf # noqa: F401 -from app.tasks.extract_metadata_with_gpt import \ - extract_metadata_with_gpt # noqa: F401 +from app.tasks.embed_metadata_into_pdf import embed_metadata_into_pdf # noqa: F401 +from app.tasks.extract_metadata_with_gpt import extract_metadata_with_gpt # noqa: F401 from app.tasks.imap_tasks import pull_all_inboxes # noqa: F401 from app.tasks.monitor_stalled_steps import monitor_stalled_steps # noqa: F401 + # **Ensure all tasks are imported before Celery starts** from app.tasks.process_document import process_document # noqa: F401 -from app.tasks.process_with_azure_document_intelligence import \ - process_with_azure_document_intelligence # noqa: F401 +from app.tasks.process_with_azure_document_intelligence import process_with_azure_document_intelligence # noqa: F401 from app.tasks.refine_text_with_gpt import refine_text_with_gpt # noqa: F401 from app.tasks.rotate_pdf_pages import rotate_pdf_pages # noqa: F401 from app.tasks.send_to_all import send_to_all_destinations # noqa: F401 + # Import new send tasks from app.tasks.upload_to_dropbox import upload_to_dropbox # noqa: F401 from app.tasks.upload_to_email import upload_to_email # noqa: F401 from app.tasks.upload_to_ftp import upload_to_ftp # noqa: F401 -from app.tasks.upload_to_google_drive import \ - upload_to_google_drive # noqa: F401 +from app.tasks.upload_to_google_drive import upload_to_google_drive # noqa: F401 from app.tasks.upload_to_nextcloud import upload_to_nextcloud # noqa: F401 from app.tasks.upload_to_onedrive import upload_to_onedrive # noqa: F401 from app.tasks.upload_to_paperless import upload_to_paperless # noqa: F401 @@ -36,6 +34,7 @@ from app.tasks.upload_to_s3 import upload_to_s3 # noqa: F401 from app.tasks.upload_to_sftp import upload_to_sftp # noqa: F401 from app.tasks.upload_to_webdav import upload_to_webdav # noqa: F401 from app.tasks.uptime_kuma_tasks import ping_uptime_kuma # noqa: F401 + # Register the settings reload signal handler so workers pick up config changes from app.utils.settings_sync import register_settings_reload_signal @@ -95,6 +94,4 @@ celery.conf.beat_schedule = { } # Remove None entries from beat_schedule -celery.conf.beat_schedule = { - k: v for k, v in celery.conf.beat_schedule.items() if v is not None -} +celery.conf.beat_schedule = {k: v for k, v in celery.conf.beat_schedule.items() if v is not None} diff --git a/app/models.py b/app/models.py index d62099d2..78c09df9 100644 --- a/app/models.py +++ b/app/models.py @@ -1,7 +1,6 @@ # app/models.py -from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String, - Text, UniqueConstraint, func) +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func from app.database import Base @@ -70,33 +69,21 @@ class FileProcessingStep(Base): id = Column(Integer, primary_key=True, index=True) file_id = Column(Integer, ForeignKey(_FILES_ID_FK), nullable=False, index=True) - step_name = Column( - String, nullable=False, index=True - ) # e.g., "hash_file", "upload_to_dropbox" - status = Column( - String, nullable=False - ) # "pending", "in_progress", "success", "failure", "skipped" + step_name = Column(String, nullable=False, index=True) # e.g., "hash_file", "upload_to_dropbox" + status = Column(String, nullable=False) # "pending", "in_progress", "success", "failure", "skipped" started_at = Column(DateTime(timezone=True), nullable=True) # When step started - completed_at = Column( - DateTime(timezone=True), nullable=True - ) # When step finished (success/failure) + completed_at = Column(DateTime(timezone=True), nullable=True) # When step finished (success/failure) error_message = Column(Text, nullable=True) # Error message if status is "failure" created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - __table_args__ = ( - UniqueConstraint("file_id", "step_name", name="unique_file_step"), - ) + __table_args__ = (UniqueConstraint("file_id", "step_name", name="unique_file_step"),) class ProcessingLog(Base): __tablename__ = "processing_logs" id = Column(Integer, primary_key=True, index=True) - file_id = Column( - Integer, ForeignKey(_FILES_ID_FK), nullable=True - ) # Optional file association + file_id = Column(Integer, ForeignKey(_FILES_ID_FK), nullable=True) # Optional file association task_id = Column(String, index=True) # Celery task ID step_name = Column(String) # e.g., "OCR", "convert_to_pdf", "upload_s3" status = Column(String) # "pending", "in_progress", "success", "failure" @@ -111,16 +98,10 @@ class ApplicationSettings(Base): __tablename__ = "application_settings" id = Column(Integer, primary_key=True, index=True) - key = Column( - String, unique=True, index=True, nullable=False - ) # Setting key (e.g., 'database_url') - value = Column( - String, nullable=True - ) # Setting value (stored as string, converted as needed) + key = Column(String, unique=True, index=True, nullable=False) # Setting key (e.g., 'database_url') + value = Column(String, nullable=True) # Setting value (stored as string, converted as needed) created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) class SettingsAuditLog(Base): @@ -132,8 +113,6 @@ class SettingsAuditLog(Base): key = Column(String, nullable=False, index=True) # Setting key that was changed old_value = Column(String, nullable=True) # Previous value (None if first-time set) new_value = Column(String, nullable=True) # New value (None if deleted) - changed_by = Column( - String, nullable=False - ) # Username of the admin who made the change + changed_by = Column(String, nullable=False) # Username of the admin who made the change changed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True) action = Column(String, nullable=False) # "update" or "delete" diff --git a/app/utils/settings_service.py b/app/utils/settings_service.py index b821bcee..b7344118 100644 --- a/app/utils/settings_service.py +++ b/app/utils/settings_service.py @@ -871,9 +871,7 @@ def get_setting_from_db(db: Session, key: str) -> Optional[str]: Setting value as string (decrypted if necessary), or None if not found """ try: - setting = ( - db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first() - ) + setting = db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first() if not setting: return None @@ -890,9 +888,7 @@ def get_setting_from_db(db: Session, key: str) -> Optional[str]: return None -def save_setting_to_db( - db: Session, key: str, value: Optional[str], changed_by: str = "system" -) -> bool: +def save_setting_to_db(db: Session, key: str, value: Optional[str], changed_by: str = "system") -> bool: """ Save or update a setting in the database. @@ -914,20 +910,15 @@ def save_setting_to_db( storage_value = value if metadata.get("sensitive", False) and value: - from app.utils.encryption import (encrypt_value, - is_encryption_available) + from app.utils.encryption import encrypt_value, is_encryption_available if is_encryption_available(): storage_value = encrypt_value(value) logger.debug(f"Encrypted sensitive setting: {key}") else: - logger.warning( - f"Storing sensitive setting {key} in plaintext (encryption unavailable)" - ) + logger.warning(f"Storing sensitive setting {key} in plaintext (encryption unavailable)") - setting = ( - db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first() - ) + setting = db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first() old_storage_value = setting.value if setting else None if setting: @@ -1015,9 +1006,7 @@ def delete_setting_from_db(db: Session, key: str, changed_by: str = "system") -> True if successful, False otherwise """ try: - setting = ( - db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first() - ) + setting = db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first() if setting: # Capture old value for audit log (decrypt if sensitive) metadata = get_setting_metadata(key) @@ -1045,9 +1034,7 @@ def delete_setting_from_db(db: Session, key: str, changed_by: str = "system") -> db.add(audit_entry) db.commit() - logger.info( - f"Deleted setting {key} from database (changed_by={changed_by})" - ) + logger.info(f"Deleted setting {key} from database (changed_by={changed_by})") return True return False except SQLAlchemyError as e: @@ -1131,9 +1118,7 @@ def validate_setting_value(key: str, value: str) -> Tuple[bool, Optional[str]]: return True, None -def get_audit_log( - db: Session, limit: int = 100, offset: int = 0 -) -> List[Dict[str, Any]]: +def get_audit_log(db: Session, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]: """ Retrieve the settings audit log, most recent first. @@ -1150,11 +1135,7 @@ def get_audit_log( """ try: entries = ( - db.query(SettingsAuditLog) - .order_by(SettingsAuditLog.changed_at.desc()) - .limit(limit) - .offset(offset) - .all() + db.query(SettingsAuditLog).order_by(SettingsAuditLog.changed_at.desc()).limit(limit).offset(offset).all() ) result = [] for entry in entries: @@ -1164,20 +1145,10 @@ def get_audit_log( { "id": entry.id, "key": entry.key, - "old_value": ( - "[REDACTED]" - if is_sensitive and entry.old_value - else entry.old_value - ), - "new_value": ( - "[REDACTED]" - if is_sensitive and entry.new_value - else entry.new_value - ), + "old_value": ("[REDACTED]" if is_sensitive and entry.old_value else entry.old_value), + "new_value": ("[REDACTED]" if is_sensitive and entry.new_value else entry.new_value), "changed_by": entry.changed_by, - "changed_at": ( - entry.changed_at.isoformat() if entry.changed_at else None - ), + "changed_at": (entry.changed_at.isoformat() if entry.changed_at else None), "action": entry.action, } ) @@ -1215,20 +1186,10 @@ def get_setting_history(db: Session, key: str) -> List[Dict[str, Any]]: { "id": entry.id, "key": entry.key, - "old_value": ( - "[REDACTED]" - if is_sensitive and entry.old_value - else entry.old_value - ), - "new_value": ( - "[REDACTED]" - if is_sensitive and entry.new_value - else entry.new_value - ), + "old_value": ("[REDACTED]" if is_sensitive and entry.old_value else entry.old_value), + "new_value": ("[REDACTED]" if is_sensitive and entry.new_value else entry.new_value), "changed_by": entry.changed_by, - "changed_at": ( - entry.changed_at.isoformat() if entry.changed_at else None - ), + "changed_at": (entry.changed_at.isoformat() if entry.changed_at else None), "action": entry.action, } ) @@ -1238,9 +1199,7 @@ def get_setting_history(db: Session, key: str) -> List[Dict[str, Any]]: return [] -def rollback_setting( - db: Session, key: str, history_id: int, changed_by: str = "system" -) -> bool: +def rollback_setting(db: Session, key: str, history_id: int, changed_by: str = "system") -> bool: """ Revert a setting to the value recorded in a specific audit log entry. @@ -1264,14 +1223,10 @@ def rollback_setting( """ try: history_entry = ( - db.query(SettingsAuditLog) - .filter(SettingsAuditLog.id == history_id, SettingsAuditLog.key == key) - .first() + db.query(SettingsAuditLog).filter(SettingsAuditLog.id == history_id, SettingsAuditLog.key == key).first() ) if not history_entry: - logger.warning( - f"Rollback failed: audit log entry {history_id} not found for key '{key}'" - ) + logger.warning(f"Rollback failed: audit log entry {history_id} not found for key '{key}'") return False target_value = history_entry.new_value @@ -1282,9 +1237,7 @@ def rollback_setting( else: return save_setting_to_db(db, key, target_value, changed_by=changed_by) except SQLAlchemyError as e: - logger.error( - f"Error rolling back setting {key} to history entry {history_id}: {e}" - ) + logger.error(f"Error rolling back setting {key} to history entry {history_id}: {e}") db.rollback() return False diff --git a/app/views/settings.py b/app/views/settings.py index 3b40a94d..dd34ed03 100644 --- a/app/views/settings.py +++ b/app/views/settings.py @@ -12,12 +12,13 @@ from fastapi.responses import RedirectResponse from sqlalchemy.orm import Session from app.utils.config_validator.masking import mask_sensitive_value -from app.utils.settings_service import (SETTING_METADATA, - get_all_settings_from_db, - get_setting_metadata, - get_settings_by_category) -from app.views.base import (APIRouter, get_db, require_login, settings, - templates) +from app.utils.settings_service import ( + SETTING_METADATA, + get_all_settings_from_db, + get_setting_metadata, + get_settings_by_category, +) +from app.views.base import APIRouter, get_db, require_login, settings, templates logger = logging.getLogger(__name__) router = APIRouter() @@ -102,9 +103,7 @@ async def settings_page(request: Request, db: Session = Depends(get_db)): settings_data[category].append( { "key": key, - "display_value": ( - display_value if display_value is not None else "" - ), + "display_value": (display_value if display_value is not None else ""), "metadata": metadata, "source": source, "source_label": source_label, @@ -175,9 +174,7 @@ async def credentials_page(request: Request, db: Session = Depends(get_db)): ) total = sum(len(v) for v in categories.values()) - configured_count = sum( - 1 for creds in categories.values() for c in creds if c["configured"] - ) + configured_count = sum(1 for creds in categories.values() for c in creds if c["configured"]) return templates.TemplateResponse( "credentials.html", diff --git a/app/views/wizard.py b/app/views/wizard.py index 3105e7c4..7d70d463 100644 --- a/app/views/wizard.py +++ b/app/views/wizard.py @@ -40,11 +40,7 @@ async def setup_wizard(request: Request, step: int = 1, db: Session = Depends(ge current_settings = wizard_steps.get(step, []) # Get step category (all settings in a step should have same category) - step_category = ( - current_settings[0].get("wizard_category", "Configuration") - if current_settings - else "Configuration" - ) + step_category = current_settings[0].get("wizard_category", "Configuration") if current_settings else "Configuration" # Enrich settings with current live values from app.config import settings as app_settings @@ -68,9 +64,7 @@ async def setup_wizard(request: Request, step: int = 1, db: Session = Depends(ge else: current_value = "" value_source = "none" - enriched_settings.append( - {**s, "current_value": current_value, "value_source": value_source} - ) + enriched_settings.append({**s, "current_value": current_value, "value_source": value_source}) current_settings = enriched_settings return templates.TemplateResponse( @@ -88,9 +82,7 @@ async def setup_wizard(request: Request, step: int = 1, db: Session = Depends(ge @router.post("/setup") -async def setup_wizard_save( - request: Request, step: int = Form(...), db: Session = Depends(get_db) -): +async def setup_wizard_save(request: Request, step: int = Form(...), db: Session = Depends(get_db)): """ Save settings from the current wizard step. """ @@ -138,9 +130,7 @@ async def setup_wizard_save( except Exception as e: logger.error(f"Error saving wizard settings: {e}") - return RedirectResponse( - url=f"/setup?step={step}&error=save_failed", status_code=303 - ) + return RedirectResponse(url=f"/setup?step={step}&error=save_failed", status_code=303) @router.get("/setup/skip") @@ -176,9 +166,7 @@ async def setup_wizard_undo_skip(request: Request, db: Session = Depends(get_db) try: from app.utils.settings_service import delete_setting_from_db - delete_setting_from_db( - db, "_setup_wizard_skipped", changed_by="wizard_undo_skip" - ) + delete_setting_from_db(db, "_setup_wizard_skipped", changed_by="wizard_undo_skip") logger.info("Setup wizard skip marker removed; redirecting to wizard") return RedirectResponse(url="/setup?step=1", status_code=303) except Exception as e: diff --git a/tests/test_api_dropbox.py b/tests/test_api_dropbox.py index 900e3ffa..f3d3a7a1 100644 --- a/tests/test_api_dropbox.py +++ b/tests/test_api_dropbox.py @@ -249,9 +249,7 @@ class TestTestDropboxToken: mock_settings.dropbox_app_secret = "app-secret" mock_settings.http_request_timeout = 30 - mock_post.side_effect = requests.exceptions.ConnectionError( - "Connection refused" - ) + mock_post.side_effect = requests.exceptions.ConnectionError("Connection refused") response = client.get("/api/dropbox/test-token") @@ -312,9 +310,7 @@ class TestSaveDropboxSettings: assert "new-token" in content @patch("app.api.dropbox.settings") - def test_save_settings_with_all_optional_fields( - self, mock_settings, client, tmp_path - ): + def test_save_settings_with_all_optional_fields(self, mock_settings, client, tmp_path): """Test saving all Dropbox settings including optional fields.""" mock_settings.dropbox_refresh_token = "" mock_settings.dropbox_app_key = "" diff --git a/tests/test_api_onedrive_comprehensive.py b/tests/test_api_onedrive_comprehensive.py index e67e4f40..1019031b 100644 --- a/tests/test_api_onedrive_comprehensive.py +++ b/tests/test_api_onedrive_comprehensive.py @@ -72,9 +72,7 @@ class TestExchangeOneDriveToken: @patch("app.api.onedrive.exchange_oauth_token") def test_exchange_token_error(self, mock_exchange, client: TestClient): """Test token exchange with error from OAuth provider.""" - mock_exchange.side_effect = HTTPException( - status_code=400, detail="Invalid authorization code" - ) + mock_exchange.side_effect = HTTPException(status_code=400, detail="Invalid authorization code") response = client.post( "/api/onedrive/exchange-token", @@ -109,9 +107,7 @@ class TestTestOneDriveToken: @patch("requests.post") @patch("requests.get") @patch("app.config.settings") - def test_test_token_success( - self, mock_settings, mock_get, mock_post, client: TestClient - ): + def test_test_token_success(self, mock_settings, mock_get, mock_post, client: TestClient): """Test successful token validation with properly mocked responses.""" # Configure settings with property mocking type(mock_settings).onedrive_refresh_token = "test_refresh_token" @@ -162,9 +158,7 @@ class TestTestOneDriveToken: @patch("requests.post") @patch("app.config.settings") - def test_test_token_refresh_failed( - self, mock_settings, mock_post, client: TestClient - ): + def test_test_token_refresh_failed(self, mock_settings, mock_post, client: TestClient): """Test when token refresh fails.""" type(mock_settings).onedrive_refresh_token = "invalid_token" type(mock_settings).onedrive_client_id = "test_client_id" @@ -189,9 +183,7 @@ class TestTestOneDriveToken: @patch("requests.post") @patch("requests.get") @patch("app.config.settings") - def test_test_token_new_refresh_token_issued( - self, mock_settings, mock_get, mock_post, client: TestClient - ): + def test_test_token_new_refresh_token_issued(self, mock_settings, mock_get, mock_post, client: TestClient): """Test when Microsoft issues a new refresh token.""" type(mock_settings).onedrive_refresh_token = "old_refresh_token" type(mock_settings).onedrive_client_id = "test_client_id" @@ -278,9 +270,7 @@ class TestTestOneDriveToken: @patch("requests.post") @patch("requests.get") @patch("app.config.settings") - def test_test_token_user_info_failed( - self, mock_settings, mock_get, mock_post, client: TestClient - ): + def test_test_token_user_info_failed(self, mock_settings, mock_get, mock_post, client: TestClient): """Test when user info request fails.""" mock_settings.onedrive_refresh_token = "test_token" mock_settings.onedrive_client_id = "test_client_id" @@ -356,9 +346,7 @@ class TestSaveOneDriveSettings: @patch("os.path.exists") @patch("os.path.dirname") @patch("app.config.settings") - def test_save_settings_success( - self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient - ): + def test_save_settings_success(self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient): """Test successful save to .env file.""" mock_exists.return_value = True mock_dirname.return_value = "/app" @@ -380,9 +368,7 @@ class TestSaveOneDriveSettings: @patch("os.path.exists") @patch("os.path.dirname") - def test_save_settings_env_file_not_found( - self, mock_dirname, mock_exists, client: TestClient - ): + def test_save_settings_env_file_not_found(self, mock_dirname, mock_exists, client: TestClient): """Test that missing .env file is non-fatal — DB write still succeeds.""" mock_exists.return_value = False mock_dirname.return_value = "/app" @@ -468,17 +454,13 @@ class TestSaveOneDriveSettings: def test_save_settings_missing_required_field(self, client: TestClient): """Test save without required refresh_token.""" - response = client.post( - "/api/onedrive/save-settings", data={"tenant_id": "common"} - ) + response = client.post("/api/onedrive/save-settings", data={"tenant_id": "common"}) assert response.status_code == 422 # Validation error @patch("os.path.exists") @patch("os.path.dirname") - def test_save_settings_exception_handling( - self, mock_dirname, mock_exists, client: TestClient - ): + def test_save_settings_exception_handling(self, mock_dirname, mock_exists, client: TestClient): """Test that exceptions in .env write are non-fatal — DB write still succeeds.""" mock_exists.side_effect = Exception("Unexpected error") @@ -498,9 +480,7 @@ class TestUpdateOneDriveSettings: @patch("app.tasks.upload_to_onedrive.get_onedrive_token") @patch("app.config.settings") - def test_update_settings_success( - self, mock_settings, mock_get_token, client: TestClient - ): + def test_update_settings_success(self, mock_settings, mock_get_token, client: TestClient): """Test successful settings update in memory.""" mock_get_token.return_value = "test_token" @@ -521,9 +501,7 @@ class TestUpdateOneDriveSettings: @patch("app.tasks.upload_to_onedrive.get_onedrive_token") @patch("app.config.settings") - def test_update_settings_minimal( - self, mock_settings, mock_get_token, client: TestClient - ): + def test_update_settings_minimal(self, mock_settings, mock_get_token, client: TestClient): """Test update with only required fields.""" mock_get_token.return_value = "test_token" @@ -536,9 +514,7 @@ class TestUpdateOneDriveSettings: @patch("app.tasks.upload_to_onedrive.get_onedrive_token") @patch("app.config.settings") - def test_update_settings_token_test_fails( - self, mock_settings, mock_get_token, client: TestClient - ): + def test_update_settings_token_test_fails(self, mock_settings, mock_get_token, client: TestClient): """Test update when token test fails.""" mock_get_token.side_effect = Exception("Token invalid") @@ -554,16 +530,12 @@ class TestUpdateOneDriveSettings: def test_update_settings_missing_required_field(self, client: TestClient): """Test update without required refresh_token.""" - response = client.post( - "/api/onedrive/update-settings", data={"tenant_id": "common"} - ) + response = client.post("/api/onedrive/update-settings", data={"tenant_id": "common"}) assert response.status_code == 422 @patch("app.config.settings") - def test_update_settings_exception_handling( - self, mock_settings, client: TestClient - ): + def test_update_settings_exception_handling(self, mock_settings, client: TestClient): """Test exception handling in update settings.""" mock_settings.onedrive_refresh_token = None @@ -619,9 +591,7 @@ class TestGetOneDriveFullConfig: assert "status" in data @patch("app.config.settings") - def test_get_full_config_exception_handling( - self, mock_settings, client: TestClient - ): + def test_get_full_config_exception_handling(self, mock_settings, client: TestClient): """Test exception handling in get full config.""" # Even with exception, endpoint catches it response = client.get("/api/onedrive/get-full-config") @@ -676,9 +646,7 @@ class TestOneDriveIntegration: @patch("requests.post") @patch("requests.get") @patch("app.config.settings") - def test_token_refresh_rotation( - self, mock_settings, mock_get, mock_post, client: TestClient - ): + def test_token_refresh_rotation(self, mock_settings, mock_get, mock_post, client: TestClient): """Test token refresh with automatic rotation.""" type(mock_settings).onedrive_refresh_token = "old_token" type(mock_settings).onedrive_client_id = "test_client_id" diff --git a/tests/test_settings_audit_log.py b/tests/test_settings_audit_log.py index a28df696..79021423 100644 --- a/tests/test_settings_audit_log.py +++ b/tests/test_settings_audit_log.py @@ -45,9 +45,7 @@ class TestAuditLogOnSave: def test_save_creates_audit_entry(self, db_session): from app.utils.settings_service import save_setting_to_db - result = save_setting_to_db( - db_session, "workdir", "/new/path", changed_by="alice" - ) + result = save_setting_to_db(db_session, "workdir", "/new/path", changed_by="alice") assert result is True entry = db_session.query(SettingsAuditLog).filter_by(key="workdir").first() @@ -73,18 +71,13 @@ class TestAuditLogOnSave: assert update_entry.new_value == "/new/path" def test_delete_creates_audit_entry(self, db_session): - from app.utils.settings_service import (delete_setting_from_db, - save_setting_to_db) + from app.utils.settings_service import delete_setting_from_db, save_setting_to_db save_setting_to_db(db_session, "workdir", "/some/path", changed_by="admin") result = delete_setting_from_db(db_session, "workdir", changed_by="carol") assert result is True - delete_entry = ( - db_session.query(SettingsAuditLog) - .filter_by(key="workdir", action="delete") - .first() - ) + delete_entry = db_session.query(SettingsAuditLog).filter_by(key="workdir", action="delete").first() assert delete_entry is not None assert delete_entry.old_value == "/some/path" assert delete_entry.new_value is None @@ -93,9 +86,7 @@ class TestAuditLogOnSave: def test_delete_nonexistent_returns_false_no_entry(self, db_session): from app.utils.settings_service import delete_setting_from_db - result = delete_setting_from_db( - db_session, "nonexistent_key", changed_by="admin" - ) + result = delete_setting_from_db(db_session, "nonexistent_key", changed_by="admin") assert result is False assert db_session.query(SettingsAuditLog).count() == 0 @@ -119,8 +110,7 @@ class TestGetAuditLog: """get_audit_log returns entries, masks sensitive values.""" def test_returns_all_entries_most_recent_first(self, db_session): - from app.utils.settings_service import (get_audit_log, - save_setting_to_db) + from app.utils.settings_service import get_audit_log, save_setting_to_db save_setting_to_db(db_session, "workdir", "/first", changed_by="u1") save_setting_to_db(db_session, "workdir", "/second", changed_by="u2") @@ -133,12 +123,9 @@ class TestGetAuditLog: assert log[1]["new_value"] == "/first" def test_sensitive_values_are_masked(self, db_session): - from app.utils.settings_service import (get_audit_log, - save_setting_to_db) + from app.utils.settings_service import get_audit_log, save_setting_to_db - save_setting_to_db( - db_session, "openai_api_key", "sk-secret123", changed_by="admin" - ) + save_setting_to_db(db_session, "openai_api_key", "sk-secret123", changed_by="admin") log = get_audit_log(db_session) @@ -146,8 +133,7 @@ class TestGetAuditLog: assert entry["new_value"] == "[REDACTED]" def test_required_fields_present(self, db_session): - from app.utils.settings_service import (get_audit_log, - save_setting_to_db) + from app.utils.settings_service import get_audit_log, save_setting_to_db save_setting_to_db(db_session, "workdir", "/path", changed_by="alice") @@ -167,8 +153,7 @@ class TestGetAuditLog: assert field in entry def test_limit_and_offset(self, db_session): - from app.utils.settings_service import (get_audit_log, - save_setting_to_db) + from app.utils.settings_service import get_audit_log, save_setting_to_db for i in range(5): save_setting_to_db(db_session, "workdir", f"/path{i}", changed_by="admin") @@ -190,8 +175,7 @@ class TestGetSettingHistory: """get_setting_history returns only entries for the requested key.""" def test_returns_only_matching_key(self, db_session): - from app.utils.settings_service import (get_setting_history, - save_setting_to_db) + from app.utils.settings_service import get_setting_history, save_setting_to_db save_setting_to_db(db_session, "workdir", "/wdir", changed_by="admin") save_setting_to_db(db_session, "debug", "true", changed_by="admin") @@ -219,32 +203,21 @@ class TestRollbackSetting: """rollback_setting reinstates the value from a given audit log entry.""" def test_rollback_to_previous_value(self, db_session): - from app.utils.settings_service import (get_setting_from_db, - rollback_setting, - save_setting_to_db) + from app.utils.settings_service import get_setting_from_db, rollback_setting, save_setting_to_db - save_setting_to_db( - db_session, "workdir", "/v1", changed_by="admin" - ) # entry id 1 - save_setting_to_db( - db_session, "workdir", "/v2", changed_by="admin" - ) # entry id 2 + save_setting_to_db(db_session, "workdir", "/v1", changed_by="admin") # entry id 1 + save_setting_to_db(db_session, "workdir", "/v2", changed_by="admin") # entry id 2 - first_entry = ( - db_session.query(SettingsAuditLog).filter_by(key="workdir").first() - ) + first_entry = db_session.query(SettingsAuditLog).filter_by(key="workdir").first() # first entry has new_value="/v1" - success = rollback_setting( - db_session, "workdir", first_entry.id, changed_by="rollbacker" - ) + success = rollback_setting(db_session, "workdir", first_entry.id, changed_by="rollbacker") assert success is True current = get_setting_from_db(db_session, "workdir") assert current == "/v1" def test_rollback_creates_new_audit_entry(self, db_session): - from app.utils.settings_service import (rollback_setting, - save_setting_to_db) + from app.utils.settings_service import rollback_setting, save_setting_to_db save_setting_to_db(db_session, "workdir", "/v1", changed_by="admin") entry = db_session.query(SettingsAuditLog).filter_by(key="workdir").first() @@ -255,8 +228,7 @@ class TestRollbackSetting: assert db_session.query(SettingsAuditLog).count() == initial_count + 1 def test_rollback_wrong_history_id_returns_false(self, db_session): - from app.utils.settings_service import (rollback_setting, - save_setting_to_db) + from app.utils.settings_service import rollback_setting, save_setting_to_db save_setting_to_db(db_session, "workdir", "/v1", changed_by="admin") @@ -265,8 +237,7 @@ class TestRollbackSetting: assert result is False def test_rollback_wrong_key_returns_false(self, db_session): - from app.utils.settings_service import (rollback_setting, - save_setting_to_db) + from app.utils.settings_service import rollback_setting, save_setting_to_db save_setting_to_db(db_session, "workdir", "/v1", changed_by="admin") entry = db_session.query(SettingsAuditLog).filter_by(key="workdir").first() @@ -287,8 +258,7 @@ class TestNotifySettingsUpdated: """notify_settings_updated publishes the settings version key to Redis.""" def test_sets_redis_key(self): - from app.utils.settings_sync import (SETTINGS_VERSION_KEY, - notify_settings_updated) + from app.utils.settings_sync import SETTINGS_VERSION_KEY, notify_settings_updated mock_redis = MagicMock() mock_redis_instance = MagicMock() @@ -394,9 +364,7 @@ class TestHistoryEndpoint: mock_db = MagicMock() mock_admin = {"is_admin": True} - result = asyncio.run( - get_key_history("workdir", mock_request, mock_db, mock_admin) - ) + result = asyncio.run(get_key_history("workdir", mock_request, mock_db, mock_admin)) assert result["key"] == "workdir" assert len(result["history"]) == 1 @@ -421,9 +389,7 @@ class TestRollbackEndpoint: mock_db = MagicMock() mock_admin = {"is_admin": True} - result = asyncio.run( - rollback_setting_to_history("workdir", 1, mock_request, mock_db, mock_admin) - ) + result = asyncio.run(rollback_setting_to_history("workdir", 1, mock_request, mock_db, mock_admin)) assert result["success"] is True mock_notify.assert_called_once() @@ -443,10 +409,6 @@ class TestRollbackEndpoint: mock_admin = {"is_admin": True} with pytest.raises(HTTPException) as exc_info: - asyncio.run( - rollback_setting_to_history( - "workdir", 9999, mock_request, mock_db, mock_admin - ) - ) + asyncio.run(rollback_setting_to_history("workdir", 9999, mock_request, mock_db, mock_admin)) assert exc_info.value.status_code == 404 diff --git a/tests/test_wizard_db_persist.py b/tests/test_wizard_db_persist.py index 6cb182c8..2f51f09e 100644 --- a/tests/test_wizard_db_persist.py +++ b/tests/test_wizard_db_persist.py @@ -48,9 +48,7 @@ class TestSetupWizardDbPersist: @patch("app.views.wizard.notify_settings_updated") @patch("app.views.wizard.save_setting_to_db") - def test_notify_not_called_when_no_settings_saved( - self, mock_save, mock_notify, client - ): + def test_notify_not_called_when_no_settings_saved(self, mock_save, mock_notify, client): """Test that notify_settings_updated is NOT called when saved_count == 0.""" mock_save.return_value = False @@ -65,9 +63,7 @@ class TestSetupWizardDbPersist: @patch("app.views.wizard.notify_settings_updated") @patch("app.views.wizard.secrets.token_hex") @patch("app.views.wizard.save_setting_to_db") - def test_auto_generate_session_secret( - self, mock_save, mock_token, mock_notify, client - ): + def test_auto_generate_session_secret(self, mock_save, mock_token, mock_notify, client): """Test that session_secret auto-generate path produces a real token.""" mock_save.return_value = True mock_token.return_value = "deadbeef" * 8 @@ -136,9 +132,7 @@ class TestDropboxSaveSettingsDbPersist: @patch("app.api.dropbox.settings") @patch("app.api.dropbox.notify_settings_updated") @patch("app.api.dropbox.save_setting_to_db") - def test_db_written_even_when_env_missing( - self, mock_save, mock_notify, mock_settings, client - ): + def test_db_written_even_when_env_missing(self, mock_save, mock_notify, mock_settings, client): """Test that DB is written even when .env doesn't exist (no exception).""" mock_save.return_value = True @@ -157,9 +151,7 @@ class TestDropboxSaveSettingsDbPersist: @patch("app.api.dropbox.settings") @patch("app.api.dropbox.notify_settings_updated") @patch("app.api.dropbox.save_setting_to_db") - def test_notify_settings_updated_called( - self, mock_save, mock_notify, mock_settings, client - ): + def test_notify_settings_updated_called(self, mock_save, mock_notify, mock_settings, client): """Test that notify_settings_updated is called.""" mock_save.return_value = True @@ -175,9 +167,7 @@ class TestDropboxSaveSettingsDbPersist: @patch("app.api.dropbox.settings") @patch("app.api.dropbox.notify_settings_updated") @patch("app.api.dropbox.save_setting_to_db") - def test_all_provided_values_persisted( - self, mock_save, mock_notify, mock_settings, client - ): + def test_all_provided_values_persisted(self, mock_save, mock_notify, mock_settings, client): """Test that all provided values are persisted to DB.""" mock_save.return_value = True @@ -212,9 +202,7 @@ class TestGoogleDriveUpdateSettingsDbPersist: @patch("app.api.google_drive.settings") @patch("app.api.google_drive.notify_settings_updated") @patch("app.api.google_drive.save_setting_to_db") - def test_db_written_for_each_provided_field( - self, mock_save, mock_notify, mock_settings, client - ): + def test_db_written_for_each_provided_field(self, mock_save, mock_notify, mock_settings, client): """Test that DB is written for each provided field.""" mock_save.return_value = True @@ -241,9 +229,7 @@ class TestGoogleDriveUpdateSettingsDbPersist: @patch("app.api.google_drive.settings") @patch("app.api.google_drive.notify_settings_updated") @patch("app.api.google_drive.save_setting_to_db") - def test_use_oauth_saved_as_lowercase_string( - self, mock_save, mock_notify, mock_settings, client - ): + def test_use_oauth_saved_as_lowercase_string(self, mock_save, mock_notify, mock_settings, client): """Test that use_oauth is saved as 'true' or 'false' string.""" mock_save.return_value = True @@ -253,11 +239,7 @@ class TestGoogleDriveUpdateSettingsDbPersist: follow_redirects=False, ) - use_oauth_calls = [ - call - for call in mock_save.call_args_list - if call[0][1] == "google_drive_use_oauth" - ] + use_oauth_calls = [call for call in mock_save.call_args_list if call[0][1] == "google_drive_use_oauth"] assert len(use_oauth_calls) == 1 assert use_oauth_calls[0][0][2] in ("true", "false") @@ -289,9 +271,7 @@ class TestOneDriveSaveSettingsDbPersist: @patch("app.api.onedrive.settings") @patch("app.api.onedrive.notify_settings_updated") @patch("app.api.onedrive.save_setting_to_db") - def test_db_written_even_without_env_file( - self, mock_save, mock_notify, mock_settings, client - ): + def test_db_written_even_without_env_file(self, mock_save, mock_notify, mock_settings, client): """Test that DB is written even when .env file does not exist.""" mock_save.return_value = True @@ -435,9 +415,7 @@ class TestExportEnvEndpoint: mock_request = MagicMock() mock_admin = {"id": "admin", "is_admin": True} - result = asyncio.run( - export_env_settings(mock_request, db_session, mock_admin, source="db") - ) + result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="db")) assert result.media_type == "text/plain" def test_content_disposition_header(self, db_session): @@ -449,9 +427,7 @@ class TestExportEnvEndpoint: mock_request = MagicMock() mock_admin = {"id": "admin", "is_admin": True} - result = asyncio.run( - export_env_settings(mock_request, db_session, mock_admin, source="db") - ) + result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="db")) cd = result.headers.get("content-disposition", "") assert "attachment" in cd assert ".env" in cd @@ -468,11 +444,7 @@ class TestExportEnvEndpoint: mock_admin = {"id": "admin", "is_admin": True} with pytest.raises(HTTPException) as exc_info: - asyncio.run( - export_env_settings( - mock_request, db_session, mock_admin, source="invalid" - ) - ) + asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="invalid")) assert exc_info.value.status_code == 400 def test_default_source_is_db(self, db_session): @@ -497,11 +469,7 @@ class TestExportEnvEndpoint: mock_request = MagicMock() mock_admin = {"id": "admin", "is_admin": True} - result = asyncio.run( - export_env_settings( - mock_request, db_session, mock_admin, source="effective" - ) - ) + result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="effective")) assert result.media_type == "text/plain" def test_output_contains_docuelevate_header(self, db_session): @@ -513,7 +481,5 @@ class TestExportEnvEndpoint: mock_request = MagicMock() mock_admin = {"id": "admin", "is_admin": True} - result = asyncio.run( - export_env_settings(mock_request, db_session, mock_admin, source="db") - ) + result = asyncio.run(export_env_settings(mock_request, db_session, mock_admin, source="db")) assert b"DocuElevate" in result.body