style: apply ruff auto-fix

- Auto-formatted code with ruff format
- Applied ruff linting fixes with --fix

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