test: add comprehensive tests for critical untested files and fix deprecated warnings
- Add tests for migrate_logs_to_steps.py, upload_to_paperless.py, dropbox API, upload_to_dropbox.py, upload_to_nextcloud.py, upload_to_onedrive.py, upload_with_rclone.py, and config_validator.py - Fix PydanticDeprecatedSince20: @validator → @field_validator in config.py, url_upload.py - Fix PydanticDeprecatedSince20: class Config → model_config = SettingsConfigDict - Fix PydanticDeprecatedSince211: filter Pydantic internals in settings_display.py - Fix MovedIn20Warning: use sqlalchemy.orm.declarative_base instead of ext.declarative Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,7 @@ from typing import Optional
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from pydantic import BaseModel, HttpUrl, validator
|
||||
from pydantic import BaseModel, HttpUrl, field_validator
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
@@ -31,7 +31,8 @@ class URLUploadRequest(BaseModel):
|
||||
url: HttpUrl
|
||||
filename: Optional[str] = None
|
||||
|
||||
@validator("url")
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_url_scheme(cls, v):
|
||||
"""Ensure only HTTP/HTTPS schemes are allowed"""
|
||||
parsed = urllib.parse.urlparse(str(v))
|
||||
|
||||
+11
-30
@@ -3,11 +3,13 @@
|
||||
import os
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
from pydantic import Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
database_url: str
|
||||
redis_url: str
|
||||
openai_api_key: str
|
||||
@@ -265,7 +267,8 @@ class Settings(BaseSettings):
|
||||
description="Stricter rate limit for authentication endpoints to prevent brute force attacks.",
|
||||
)
|
||||
|
||||
@validator("notification_urls", pre=True)
|
||||
@field_validator("notification_urls", mode="before")
|
||||
@classmethod
|
||||
def parse_notification_urls(cls, v):
|
||||
"""Parse notification URLs from string or list"""
|
||||
if isinstance(v, str):
|
||||
@@ -276,12 +279,13 @@ class Settings(BaseSettings):
|
||||
return []
|
||||
return v
|
||||
|
||||
@validator("session_secret")
|
||||
def validate_session_secret(cls, v, values):
|
||||
@field_validator("session_secret")
|
||||
@classmethod
|
||||
def validate_session_secret(cls, v, info):
|
||||
"""Validate that session_secret is set and has sufficient length when auth is enabled"""
|
||||
if values.get("auth_enabled") and not v:
|
||||
if info.data.get("auth_enabled") and not v:
|
||||
raise ValueError("SESSION_SECRET must be set when AUTH_ENABLED=True")
|
||||
if values.get("auth_enabled") and v and len(v) < 32:
|
||||
if info.data.get("auth_enabled") and v and len(v) < 32:
|
||||
raise ValueError("SESSION_SECRET must be at least 32 characters long")
|
||||
return v
|
||||
|
||||
@@ -347,28 +351,5 @@ class Settings(BaseSettings):
|
||||
# Return basic info if file not found
|
||||
return f"Version: {self.version}\nBuild Date: {self.build_date}\nGit SHA: {self.git_sha}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
# Convert string representations of booleans to actual booleans
|
||||
# and strip quotes from string values
|
||||
@classmethod
|
||||
def parse_env_var(cls, field_name: str, raw_val: str) -> Any:
|
||||
# First, strip quotes from the value if it's a string
|
||||
if isinstance(raw_val, str):
|
||||
if (raw_val.startswith('"') and raw_val.endswith('"')) or (
|
||||
raw_val.startswith("'") and raw_val.endswith("'")
|
||||
):
|
||||
raw_val = raw_val[1:-1]
|
||||
raw_val = raw_val.strip()
|
||||
|
||||
# Convert string representations of booleans to actual booleans
|
||||
if field_name.endswith("_enabled") or field_name == "debug":
|
||||
if raw_val.lower() in ("false", "0", "no", "n", "f"):
|
||||
return False
|
||||
if raw_val.lower() in ("true", "1", "yes", "y", "t"):
|
||||
return True
|
||||
return raw_val
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import os
|
||||
|
||||
from sqlalchemy import create_engine, exc
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.config import settings
|
||||
|
||||
@@ -9,12 +9,15 @@ from app.utils.config_validator.masking import mask_sensitive_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pydantic model attributes that should not be iterated as user settings
|
||||
_PYDANTIC_INTERNALS = {"model_computed_fields", "model_config", "model_extra", "model_fields", "model_fields_set"}
|
||||
|
||||
|
||||
def dump_all_settings():
|
||||
"""Log all settings values for diagnostic purposes"""
|
||||
logger.info("--- DUMPING ALL SETTINGS FOR DIAGNOSTIC PURPOSES ---")
|
||||
for key in dir(settings):
|
||||
if not key.startswith("_") and not callable(getattr(settings, key)):
|
||||
if not key.startswith("_") and key not in _PYDANTIC_INTERNALS and not callable(getattr(settings, key)):
|
||||
value = getattr(settings, key)
|
||||
# Mask sensitive values in logs
|
||||
if (
|
||||
@@ -190,8 +193,8 @@ def get_settings_for_display(show_values=False):
|
||||
key
|
||||
for key in dir(settings)
|
||||
if not key.startswith("_")
|
||||
and key not in _PYDANTIC_INTERNALS
|
||||
and not callable(getattr(settings, key))
|
||||
and key not in ["model_computed_fields", "model_config", "model_extra", "model_fields", "model_fields_set"]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user